From 3a0c94e42db94b68b4617021a9e8aca61b64cf46 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 1 Jul 2024 12:35:37 +0200 Subject: [PATCH] rename HashProcessor -> EtcShadowHashGenerator and cleanup API --- ...essor.java => EtcShadowHashGenerator.java} | 81 +++++++++---------- .../HsUnixUserHostingAssetValidator.java | 4 +- .../hs/validation/PasswordProperty.java | 6 +- .../hash/EtcShadowHashGeneratorUnitTest.java | 37 +++++++++ .../hsadminng/hash/HashProcessorUnitTest.java | 37 --------- ...sHostingAssetControllerAcceptanceTest.java | 4 +- .../validation/PasswordPropertyUnitTest.java | 6 +- 7 files changed, 83 insertions(+), 92 deletions(-) rename src/main/java/net/hostsharing/hsadminng/hash/{HashProcessor.java => EtcShadowHashGenerator.java} (50%) create mode 100644 src/test/java/net/hostsharing/hsadminng/hash/EtcShadowHashGeneratorUnitTest.java delete mode 100644 src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java b/src/main/java/net/hostsharing/hsadminng/hash/EtcShadowHashGenerator.java similarity index 50% rename from src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java rename to src/main/java/net/hostsharing/hsadminng/hash/EtcShadowHashGenerator.java index 6ef0a49b..cc428f0e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java +++ b/src/main/java/net/hostsharing/hsadminng/hash/EtcShadowHashGenerator.java @@ -1,17 +1,15 @@ package net.hostsharing.hsadminng.hash; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.Arrays; import java.util.PriorityQueue; import java.util.Queue; import java.util.random.RandomGenerator; -import lombok.SneakyThrows; import org.bouncycastle.crypto.generators.OpenBSDBCrypt; -import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; -public class HashProcessor { +public class EtcShadowHashGenerator { private static final RandomGenerator random = new SecureRandom(); private static final Queue predefinedSalts = new PriorityQueue<>(); @@ -19,17 +17,22 @@ public class HashProcessor { public static final int SALT_LENGTH = 16; public static final int COST_FACTOR = 13; - private final Algorithm algorithm; - private String password; + private final String plaintextPassword; + private Algorithm algorithm; public enum Algorithm { - SHA512("$6$"); + SHA512("6"); final String prefix; Algorithm(final String prefix) { this.prefix = prefix; } + + static Algorithm byPrefix(final String prefix) { + return Arrays.stream(Algorithm.values()).filter(a -> a.prefix.equals(prefix)).findAny() + .orElseThrow(() -> new IllegalArgumentException("unknown hash algorithm: '" + prefix + "'")); + } } private static final String SALT_CHARACTERS = @@ -39,42 +42,55 @@ public class HashProcessor { private String salt; - @SneakyThrows - public static HashProcessor hashAlgorithm(final Algorithm algorithm) { - return new HashProcessor(algorithm); + public static EtcShadowHashGenerator hash(final String plaintextPassword) { + return new EtcShadowHashGenerator(plaintextPassword); } - private HashProcessor(final Algorithm algorithm) throws NoSuchAlgorithmException { - this.algorithm = algorithm; + private EtcShadowHashGenerator(final String plaintextPassword) { + this.plaintextPassword = plaintextPassword; } - public HashProcessor generate(final String password) { - this.password = password; + public EtcShadowHashGenerator using(final Algorithm algorithm) { + this.algorithm = algorithm; return this; } - public String forLinux() { + void verify(final String givenHash) { + final var parts = givenHash.split("\\$"); + if (parts.length != 4) { + throw new IllegalArgumentException("not a "+algorithm.name()+" Linux hash: " + givenHash); + } + + algorithm = Algorithm.byPrefix(parts[1]); + salt = parts[2]; + + if (!generate().equals(givenHash)) { + throw new IllegalArgumentException("invalid password"); + } + } + + public String generate() { if (salt == null) { throw new IllegalStateException("no salt given"); } - if (password == null) { + if (plaintextPassword == null) { throw new IllegalStateException("no password given"); } - final var hash = OpenBSDBCrypt.generate(password.toCharArray(), salt.getBytes(), COST_FACTOR); + final var hash = OpenBSDBCrypt.generate(plaintextPassword.toCharArray(), salt.getBytes(), COST_FACTOR); final var hashedPassword = new String(org.bouncycastle.util.encoders.Base64.encode(hash.getBytes())); - return "$6$" + salt + "$" + hashedPassword; + return "$" + algorithm.prefix + "$" + salt + "$" + hashedPassword; } public static void nextSalt(final String salt) { predefinedSalts.add(salt); } - public HashProcessor withSalt(final String salt) { + public EtcShadowHashGenerator withSalt(final String salt) { this.salt = salt; return this; } - public HashProcessor withRandomSalt() { + public EtcShadowHashGenerator withRandomSalt() { if (!predefinedSalts.isEmpty()) { return withSalt(predefinedSalts.poll()); } @@ -85,29 +101,4 @@ public class HashProcessor { } return withSalt(stringBuilder.toString()); } - - public HashVerifier withHash(final String hash) { - return new HashVerifier(hash); - } - - public class HashVerifier { - - private final String hash; - - public HashVerifier(final String hash) { - final var parts = hash.split("\\$"); - if (!hash.startsWith(algorithm.prefix) || parts.length != 4) { - throw new IllegalArgumentException("not a "+algorithm.name()+" Linux hash: " + hash); - } - this.hash = hash; - withSalt(parts[2]); - } - - public void verify(final String password) { - final String recalculatedHash = hashAlgorithm(SHA512).withSalt(salt).generate(password).forLinux(); - if (!recalculatedHash.equals(this.hash)) { - throw new IllegalArgumentException("invalid password"); - } - } - } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index 1b7b01dc..8b436f16 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; -import net.hostsharing.hsadminng.hash.HashProcessor; +import net.hostsharing.hsadminng.hash.EtcShadowHashGenerator; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; @@ -31,7 +31,7 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { .withDefault("/bin/false"), stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir), stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).undisclosed().writeOnly().optional(), - passwordProperty("password").minLength(8).maxLength(40).hashedUsing(HashProcessor.Algorithm.SHA512).writeOnly()); + passwordProperty("password").minLength(8).maxLength(40).hashedUsing(EtcShadowHashGenerator.Algorithm.SHA512).writeOnly()); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index f40f5e30..f60e93c6 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -1,13 +1,13 @@ package net.hostsharing.hsadminng.hs.validation; -import net.hostsharing.hsadminng.hash.HashProcessor.Algorithm; +import net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.Algorithm; import lombok.Setter; import java.util.List; import java.util.stream.Stream; import static java.util.Optional.ofNullable; -import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; +import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.hash; import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntry; @Setter @@ -36,7 +36,7 @@ public class PasswordProperty extends StringProperty { this.hashedUsing = algorithm; computedBy((entity) -> ofNullable(entity.getDirectValue(propertyName, String.class)) - .map(password -> hashAlgorithm(algorithm).withRandomSalt().generate(password).forLinux()) + .map(password -> hash(password).using(algorithm).withRandomSalt().generate()) .orElse(null)); return self(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hash/EtcShadowHashGeneratorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/EtcShadowHashGeneratorUnitTest.java new file mode 100644 index 00000000..9b71591b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hash/EtcShadowHashGeneratorUnitTest.java @@ -0,0 +1,37 @@ +package net.hostsharing.hsadminng.hash; + +import org.junit.jupiter.api.Test; + +import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.Algorithm.SHA512; +import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.hash; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class EtcShadowHashGeneratorUnitTest { + + final String GIVEN_PASSWORD = "given password"; + final String WRONG_PASSWORD = "wrong password"; + final String GIVEN_SALT = "0123456789abcdef"; + + @Test + void verifiesHashedPasswordWithRandomSalt() { + final var hash = hash(GIVEN_PASSWORD).using(SHA512).withRandomSalt().generate(); + hash(GIVEN_PASSWORD).verify(hash); // throws exception if wrong + } + + @Test + void verifiesHashedPasswordWithGivenSalt() { + final var givenPasswordHash =hash(GIVEN_PASSWORD).using(SHA512).withSalt(GIVEN_SALT).generate(); + hash(GIVEN_PASSWORD).verify(givenPasswordHash); // throws exception if wrong + } + + @Test + void throwsExceptionForInvalidPassword() { + final var givenPasswordHash = hash(GIVEN_PASSWORD).using(SHA512).withRandomSalt().generate(); + + final var throwable = catchThrowable(() -> + hash(WRONG_PASSWORD).verify(givenPasswordHash) // throws exception if wrong); + ); + assertThat(throwable).hasMessage("invalid password"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java deleted file mode 100644 index ffffbad2..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.hostsharing.hsadminng.hash; - -import org.junit.jupiter.api.Test; - -import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; -import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; - -class HashProcessorUnitTest { - - final String GIVEN_PASSWORD = "given password"; - final String WRONG_PASSWORD = "wrong password"; - final String GIVEN_SALT = "0123456789abcdef"; - - @Test - void verifiesHashedPasswordWithRandomSalt() { - final var hash = hashAlgorithm(SHA512).withRandomSalt().generate(GIVEN_PASSWORD).forLinux(); - hashAlgorithm(SHA512).withHash(hash).verify(GIVEN_PASSWORD); // throws exception if wrong - } - - @Test - void verifiesHashedPasswordWithGivenSalt() { - final var givenPasswordHash = hashAlgorithm(SHA512).withSalt(GIVEN_SALT).generate(GIVEN_PASSWORD).forLinux(); - hashAlgorithm(SHA512).withHash(givenPasswordHash).verify(GIVEN_PASSWORD); // throws exception if wrong - } - - @Test - void throwsExceptionForInvalidPassword() { - final var givenPasswordHash = hashAlgorithm(SHA512).withSalt(GIVEN_SALT).generate(GIVEN_PASSWORD).forLinux(); - - final var throwable = catchThrowable(() -> - hashAlgorithm(SHA512).withHash(givenPasswordHash).verify(WRONG_PASSWORD)); - - assertThat(throwable).hasMessage("invalid password"); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 5743fbfc..0eb33b41 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import io.restassured.RestAssured; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; -import net.hostsharing.hsadminng.hash.HashProcessor; +import net.hostsharing.hsadminng.hash.EtcShadowHashGenerator; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; @@ -524,7 +524,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .identifier("fir01-temp") .caption("some test-unix-user") .build()); - HashProcessor.nextSalt("Jr5w/Y8zo8pCkqg7"); + EtcShadowHashGenerator.nextSalt("Jr5w/Y8zo8pCkqg7"); RestAssured // @formatter:off .given() diff --git a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java index b694c304..6348156d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java @@ -8,8 +8,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; -import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; +import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.Algorithm.SHA512; +import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.hash; import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty; import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry; import static org.assertj.core.api.Assertions.assertThat; @@ -115,6 +115,6 @@ class PasswordPropertyUnitTest { }); // then - hashAlgorithm(SHA512).withHash(result).verify("some password"); // throws exception if wrong + hash("some password").using(SHA512).withRandomSalt().generate(); // throws exception if wrong } }