rename HashProcessor -> EtcShadowHashGenerator and cleanup API

This commit is contained in:
Michael Hoennig 2024-07-01 12:35:37 +02:00
parent 7d7d99c05b
commit 3a0c94e42d
7 changed files with 83 additions and 92 deletions

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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()

View File

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