Compare commits

...

2 Commits

Author SHA1 Message Date
Michael Hoennig
52fa265a3d implement prepareProperties for converting plaintext password to hash before saving 2024-07-01 09:38:40 +02:00
Michael Hoennig
c53b2bdd62 some cleanup 2024-07-01 09:08:13 +02:00
6 changed files with 34 additions and 9 deletions

View File

@ -2,6 +2,9 @@ package net.hostsharing.hsadminng.hash;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.random.RandomGenerator;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
@ -10,7 +13,9 @@ import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512;
public class HashProcessor { public class HashProcessor {
private static final SecureRandom secureRandom = new SecureRandom(); private static final RandomGenerator random = new SecureRandom();
private static final Queue<String> predefinedSalts = new PriorityQueue<>();
public static final int SALT_LENGTH = 16; public static final int SALT_LENGTH = 16;
public static final int COST_FACTOR = 13; public static final int COST_FACTOR = 13;
@ -60,15 +65,22 @@ public class HashProcessor {
return "$6$" + salt + "$" + hashedPassword; return "$6$" + salt + "$" + hashedPassword;
} }
public static void nextSalt(final String salt) {
predefinedSalts.add(salt);
}
public HashProcessor withSalt(final String salt) { public HashProcessor withSalt(final String salt) {
this.salt = salt; this.salt = salt;
return this; return this;
} }
public HashProcessor withRandomSalt() { public HashProcessor withRandomSalt() {
if (!predefinedSalts.isEmpty()) {
return withSalt(predefinedSalts.poll());
}
final var stringBuilder = new StringBuilder(SALT_LENGTH); final var stringBuilder = new StringBuilder(SALT_LENGTH);
for (int i = 0; i < SALT_LENGTH; ++i) { for (int i = 0; i < SALT_LENGTH; ++i) {
int randomIndex = secureRandom.nextInt(SALT_CHARACTERS.length()); int randomIndex = random.nextInt(SALT_CHARACTERS.length());
stringBuilder.append(SALT_CHARACTERS.charAt(randomIndex)); stringBuilder.append(SALT_CHARACTERS.charAt(randomIndex));
} }
return withSalt(stringBuilder.toString()); return withSalt(stringBuilder.toString());

View File

@ -29,8 +29,9 @@ public class HsHostingAssetEntityProcessor {
} }
/// hashing passwords etc. /// hashing passwords etc.
@SuppressWarnings("unchecked")
public HsHostingAssetEntityProcessor prepareForSave() { public HsHostingAssetEntityProcessor prepareForSave() {
// FIXME: add computed properties validator.prepareProperties(entity);
return this; return this;
} }

View File

@ -90,13 +90,20 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
throw new IllegalArgumentException("Integer value (or null) expected, but got " + value); throw new IllegalArgumentException("Integer value (or null) expected, but got " + value);
} }
public void prepareProperties(final E entity) {
stream(propertyValidators).forEach(p -> {
if ( p.isWriteOnly() && p.isComputed()) {
entity.directProps().put(p.propertyName, p.compute(entity));
}
});
}
public Map<String, Object> revampProperties(final E entity, final Map<String, Object> config) { public Map<String, Object> revampProperties(final E entity, final Map<String, Object> config) {
final var copy = new HashMap<>(config); final var copy = new HashMap<>(config);
stream(propertyValidators).forEach(p -> { stream(propertyValidators).forEach(p -> {
// FIXME: maybe move to ValidatableProperty.postProcess(...)? if (p.isWriteOnly()) {
if ( p.isWriteOnly()) {
copy.remove(p.propertyName); copy.remove(p.propertyName);
} else if (p.isComputed()) { } else if (p.isReadOnly() && p.isComputed()) {
copy.put(p.propertyName, p.compute(entity)); copy.put(p.propertyName, p.compute(entity));
} }
}); });

View File

@ -34,7 +34,6 @@ public class PasswordProperty extends StringProperty<PasswordProperty> {
public PasswordProperty hashedUsing(final Algorithm algorithm) { public PasswordProperty hashedUsing(final Algorithm algorithm) {
this.hashedUsing = algorithm; this.hashedUsing = algorithm;
// FIXME: computedBy is too late, we need preprocess
computedBy((entity) computedBy((entity)
-> ofNullable(entity.getDirectValue(propertyName, String.class)) -> ofNullable(entity.getDirectValue(propertyName, String.class))
.map(password -> hashAlgorithm(algorithm).withRandomSalt().generate(password).forLinux()) .map(password -> hashAlgorithm(algorithm).withRandomSalt().generate(password).forLinux())

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.hash.HashProcessor;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
@ -523,6 +524,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.identifier("fir01-temp") .identifier("fir01-temp")
.caption("some test-unix-user") .caption("some test-unix-user")
.build()); .build());
HashProcessor.nextSalt("Jr5w/Y8zo8pCkqg7");
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -575,7 +577,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
assertThat(asset.getCaption()).isEqualTo("some patched test-unix-user"); assertThat(asset.getCaption()).isEqualTo("some patched test-unix-user");
assertThat(asset.getConfig().toString()).isEqualTo(""" assertThat(asset.getConfig().toString()).isEqualTo("""
{ {
"password": "Ein Passwort mit 4 Zeichengruppen!", "password": "$6$Jr5w/Y8zo8pCkqg7$JDJ5JDEzJFFsR3pidzdYTUZudE1GL0JZMURsTHVkZUpwTmVZYlM4ZzRqeC95WUo3Y2c5OUpTdUZpbWFx",
"shell": "/bin/bash", "shell": "/bin/bash",
"totpKey": "0x1234567890abcdef0123456789abcdef" "totpKey": "0x1234567890abcdef0123456789abcdef"
} }

View File

@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_WEBSPACE_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_WEBSPACE_BOOKING_ITEM;
@ -46,7 +47,10 @@ class HsUnixUserHostingAssetValidatorUnitTest {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when // when
final var result = validator.validateEntity(unixUserHostingAsset); // FIXME: validateContext final var result = Stream.concat(
validator.validateEntity(unixUserHostingAsset).stream(),
validator.validateContext(unixUserHostingAsset).stream()
).toList();
// then // then
assertThat(result).isEmpty(); assertThat(result).isEmpty();