From 3391ec6cc90e0faeb06c879529adc201c75f6660 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 28 Jun 2024 11:00:15 +0200 Subject: [PATCH] implement password-hashing (not fully integrated yet) (#67) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/67 Reviewed-by: Timotheus Pokorra --- build.gradle | 1 + .../hsadminng/hash/HashProcessor.java | 107 ++++++++++++++++++ .../HsBookingItemEntityValidator.java | 4 +- .../HsHostingAssetEntityValidator.java | 6 +- .../HsUnixUserHostingAssetValidator.java | 3 +- .../hs/validation/BooleanProperty.java | 4 +- .../hs/validation/EnumerationProperty.java | 10 +- .../hs/validation/HsEntityValidator.java | 10 +- .../hs/validation/IntegerProperty.java | 4 +- .../hs/validation/PasswordProperty.java | 24 +++- .../hs/validation/StringProperty.java | 31 ++--- .../hs/validation/ValidatableProperty.java | 51 +++++---- .../hostsharing/hsadminng/mapper/Array.java | 15 +++ .../hsadminng/hash/HashProcessorUnitTest.java | 41 +++++++ ...UnixUserHostingAssetValidatorUnitTest.java | 2 +- .../validation/PasswordPropertyUnitTest.java | 30 ++++- 16 files changed, 281 insertions(+), 62 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java diff --git a/build.gradle b/build.gradle index 332a5410..a4cc262f 100644 --- a/build.gradle +++ b/build.gradle @@ -66,6 +66,7 @@ dependencies { implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0' implementation 'org.openapitools:jackson-databind-nullable:0.2.6' implementation 'org.apache.commons:commons-text:1.11.0' + implementation 'commons-codec:commons-codec:1.17.0' implementation 'org.modelmapper:modelmapper:3.2.0' implementation 'org.iban4j:iban4j:3.2.7-RELEASE' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java b/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java new file mode 100644 index 00000000..d10ee565 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java @@ -0,0 +1,107 @@ +package net.hostsharing.hsadminng.hash; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +import lombok.SneakyThrows; + +import jakarta.validation.ValidationException; + +import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; + +public class HashProcessor { + + private static final SecureRandom secureRandom = new SecureRandom(); + + public enum Algorithm { + SHA512 + } + + private static final Base64.Encoder BASE64 = Base64.getEncoder(); + private static final String SALT_CHARACTERS = + "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789$_"; + + private final MessageDigest generator; + private byte[] saltBytes; + + @SneakyThrows + public static HashProcessor hashAlgorithm(final Algorithm algorithm) { + return new HashProcessor(algorithm); + } + + private HashProcessor(final Algorithm algorithm) throws NoSuchAlgorithmException { + generator = MessageDigest.getInstance(algorithm.name()); + } + + public String generate(final String password) { + final byte[] saltedPasswordDigest = calculateSaltedDigest(password); + final byte[] hashBytes = appendSaltToSaltedDigest(saltedPasswordDigest); + return BASE64.encodeToString(hashBytes); + } + + private byte[] appendSaltToSaltedDigest(final byte[] saltedPasswordDigest) { + final byte[] hashBytes = new byte[saltedPasswordDigest.length + 1 + saltBytes.length]; + System.arraycopy(saltedPasswordDigest, 0, hashBytes, 0, saltedPasswordDigest.length); + hashBytes[saltedPasswordDigest.length] = ':'; + System.arraycopy(saltBytes, 0, hashBytes, saltedPasswordDigest.length+1, saltBytes.length); + return hashBytes; + } + + private byte[] calculateSaltedDigest(final String password) { + generator.reset(); + generator.update(password.getBytes()); + generator.update(saltBytes); + return generator.digest(); + } + + public HashProcessor withSalt(final byte[] saltBytes) { + this.saltBytes = saltBytes; + return this; + } + + public HashProcessor withSalt(final String salt) { + return withSalt(salt.getBytes()); + } + + public HashProcessor withRandomSalt() { + final var stringBuilder = new StringBuilder(16); + for (int i = 0; i < 16; ++i) { + int randomIndex = secureRandom.nextInt(SALT_CHARACTERS.length()); + stringBuilder.append(SALT_CHARACTERS.charAt(randomIndex)); + } + return withSalt(stringBuilder.toString()); + } + + public HashVerifier withHash(final String hash) { + return new HashVerifier(hash); + } + + private static String getLastPart(String input, char delimiter) { + final var lastIndex = input.lastIndexOf(delimiter); + if (lastIndex == -1) { + throw new IllegalArgumentException("cannot determine salt, expected: 'digest:salt', but no ':' found"); + } + return input.substring(lastIndex + 1); + } + + public class HashVerifier { + + private final String hash; + + public HashVerifier(final String hash) { + this.hash = hash; + withSalt(getLastPart(new String(Base64.getDecoder().decode(hash)), ':')); + } + + public void verify(String password) { + final var computedHash = hashAlgorithm(SHA512).withSalt(saltBytes).generate(password); + if ( !computedHash.equals(hash) ) { + throw new ValidationException("invalid password"); + } + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java index 315de471..5cd0d71a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java @@ -16,7 +16,7 @@ import static java.util.Optional.ofNullable; public class HsBookingItemEntityValidator extends HsEntityValidator { - public HsBookingItemEntityValidator(final ValidatableProperty... properties) { + public HsBookingItemEntityValidator(final ValidatableProperty... properties) { super(properties); } @@ -54,7 +54,7 @@ public class HsBookingItemEntityValidator extends HsEntityValidator propDef) { + final ValidatableProperty propDef) { final var propName = propDef.propertyName(); final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse(""); final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList()) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java index 05bcee97..9f4a6e61 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -24,7 +24,7 @@ import static java.util.Optional.ofNullable; public abstract class HsHostingAssetEntityValidator extends HsEntityValidator { - static final ValidatableProperty[] NO_EXTRA_PROPERTIES = new ValidatableProperty[0]; + static final ValidatableProperty[] NO_EXTRA_PROPERTIES = new ValidatableProperty[0]; private final HsHostingAssetEntityValidator.BookingItem bookingItemValidation; private final HsHostingAssetEntityValidator.ParentAsset parentAssetValidation; @@ -36,7 +36,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator... properties) { + final ValidatableProperty... properties) { super(properties); this.bookingItemValidation = bookingItemValidation; this.parentAssetValidation = parentAssetValidation; @@ -105,7 +105,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator propDef) { + final ValidatableProperty propDef) { final var propName = propDef.propertyName(); final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse(""); final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList()) 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 74e59965..1b7b01dc 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,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +import net.hostsharing.hsadminng.hash.HashProcessor; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; @@ -30,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).writeOnly()); + passwordProperty("password").minLength(8).maxLength(40).hashedUsing(HashProcessor.Algorithm.SHA512).writeOnly()); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java index 5f893d74..abe5f7b4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java @@ -9,7 +9,7 @@ import java.util.Map; import java.util.Objects; @Setter -public class BooleanProperty extends ValidatableProperty { +public class BooleanProperty extends ValidatableProperty { private static final String[] KEY_ORDER = Array.join(ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_TAIL); @@ -23,7 +23,7 @@ public class BooleanProperty extends ValidatableProperty { return new BooleanProperty(propertyName); } - public ValidatableProperty falseIf(final String refPropertyName, final String refPropertyValue) { + public BooleanProperty falseIf(final String refPropertyName, final String refPropertyValue) { this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue); return this; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java index 60af1b73..60e0f244 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -9,7 +9,7 @@ import java.util.List; import static java.util.Arrays.stream; @Setter -public class EnumerationProperty extends ValidatableProperty { +public class EnumerationProperty extends ValidatableProperty { private static final String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, @@ -26,12 +26,12 @@ public class EnumerationProperty extends ValidatableProperty { return new EnumerationProperty(propertyName); } - public ValidatableProperty values(final String... values) { + public EnumerationProperty values(final String... values) { this.values = values; return this; } - public void deferredInit(final ValidatableProperty[] allProperties) { + public void deferredInit(final ValidatableProperty[] allProperties) { if (hasDeferredInit()) { if (this.values != null) { throw new IllegalStateException("property " + this + " already has values"); @@ -40,8 +40,8 @@ public class EnumerationProperty extends ValidatableProperty { } } - public ValidatableProperty valuesFromProperties(final String propertyNamePrefix) { - this.setDeferredInit( (ValidatableProperty[] allProperties) -> stream(allProperties) + public EnumerationProperty valuesFromProperties(final String propertyNamePrefix) { + this.setDeferredInit( (ValidatableProperty[] allProperties) -> stream(allProperties) .map(ValidatableProperty::propertyName) .filter(name -> name.startsWith(propertyNamePrefix)) .map(name -> name.substring(propertyNamePrefix.length())) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 5af7118d..bf755bd2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -14,9 +14,9 @@ import static java.util.Collections.emptyList; // TODO.refa: rename to HsEntityProcessor, also subclasses public abstract class HsEntityValidator { - public final ValidatableProperty[] propertyValidators; + public final ValidatableProperty[] propertyValidators; - public HsEntityValidator(final ValidatableProperty... validators) { + public HsEntityValidator(final ValidatableProperty... validators) { propertyValidators = validators; stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators)); } @@ -68,7 +68,7 @@ public abstract class HsEntityValidator { .orElse(emptyList())); } - protected static Integer getIntegerValueWithDefault0(final ValidatableProperty prop, final Map propValues) { + protected static Integer getIntegerValueWithDefault0(final ValidatableProperty prop, final Map propValues) { final var value = prop.getValue(propValues); if (value instanceof Integer) { return (Integer) value; @@ -92,10 +92,10 @@ public abstract class HsEntityValidator { public Map postProcess(final E entity, final Map config) { final var copy = new HashMap<>(config); stream(propertyValidators).forEach(p -> { + // FIXME: maybe move to ValidatableProperty.postProcess(...)? if ( p.isWriteOnly()) { copy.remove(p.propertyName); - } - if (p.isComputed()) { + } else if (p.isComputed()) { copy.put(p.propertyName, p.compute(entity)); } }); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java index f185c469..7021f9e1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java @@ -7,7 +7,7 @@ import org.apache.commons.lang3.Validate; import java.util.List; @Setter -public class IntegerProperty extends ValidatableProperty { +public class IntegerProperty extends ValidatableProperty { private final static String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, @@ -30,7 +30,7 @@ public class IntegerProperty extends ValidatableProperty { } @Override - public void deferredInit(final ValidatableProperty[] allProperties) { + public void deferredInit(final ValidatableProperty[] allProperties) { Validate.isTrue(min == null || minFrom == null, "min and minFrom are exclusive, but both are given"); Validate.isTrue(max == null || maxFrom == null, "max and maxFrom are exclusive, but both are given"); } 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 92cafb9a..6f285595 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -1,15 +1,24 @@ package net.hostsharing.hsadminng.hs.validation; +import net.hostsharing.hsadminng.hash.HashProcessor.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.mapper.Array.insertAfterEntry; + @Setter -public class PasswordProperty extends StringProperty { +public class PasswordProperty extends StringProperty { + + private static final String[] KEY_ORDER = insertAfterEntry(StringProperty.KEY_ORDER, "computed", "hashedUsing"); + + private Algorithm hashedUsing; private PasswordProperty(final String propertyName) { - super(propertyName); + super(propertyName, KEY_ORDER); undisclosed(); } @@ -23,7 +32,15 @@ public class PasswordProperty extends StringProperty { validatePassword(result, propValue); } - // TODO.impl: only a SHA512 hash should be stored in the database, not the password itself + public PasswordProperty hashedUsing(final Algorithm algorithm) { + this.hashedUsing = algorithm; + // FIXME: computedBy is too late, we need preprocess + computedBy((entity) + -> ofNullable(entity.getDirectValue(propertyName, String.class)) + .map(password -> hashAlgorithm(algorithm).withRandomSalt().generate(password)) + .orElse(null)); + return self(); + } @Override protected String simpleTypeName() { @@ -60,6 +77,5 @@ public class PasswordProperty extends StringProperty { if (containsColon) { result.add(propertyName + "' must not contain colon (':')"); } - } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index a499d951..a8e8b359 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -6,10 +6,11 @@ import net.hostsharing.hsadminng.mapper.Array; import java.util.List; import java.util.regex.Pattern; -@Setter -public class StringProperty extends ValidatableProperty { - private static final String[] KEY_ORDER = Array.join( +@Setter +public class StringProperty

> extends ValidatableProperty { + + protected static final String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, Array.of("matchesRegEx", "minLength", "maxLength"), ValidatableProperty.KEY_ORDER_TAIL, @@ -23,23 +24,27 @@ public class StringProperty extends ValidatableProperty { super(String.class, propertyName, KEY_ORDER); } - public static StringProperty stringProperty(final String propertyName) { - return new StringProperty(propertyName); + protected StringProperty(final String propertyName, final String[] keyOrder) { + super(String.class, propertyName, keyOrder); } - public StringProperty minLength(final int minLength) { + public static StringProperty stringProperty(final String propertyName) { + return new StringProperty<>(propertyName); + } + + public P minLength(final int minLength) { this.minLength = minLength; - return this; + return self(); } - public StringProperty maxLength(final int maxLength) { + public P maxLength(final int maxLength) { this.maxLength = maxLength; - return this; + return self(); } - public StringProperty matchesRegEx(final String regExPattern) { + public P matchesRegEx(final String regExPattern) { this.matchesRegEx = Pattern.compile(regExPattern); - return this; + return self(); } /** @@ -47,9 +52,9 @@ public class StringProperty extends ValidatableProperty { * * @return this; */ - public StringProperty undisclosed() { + public P undisclosed() { this.undisclosed = true; - return this; + return self(); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index b34eb8fa..76fc451e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -25,7 +25,7 @@ import static java.util.Optional.ofNullable; @Getter @RequiredArgsConstructor -public abstract class ValidatableProperty { +public abstract class ValidatableProperty

, T> { protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName"); protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage"); @@ -51,7 +51,7 @@ public abstract class ValidatableProperty { @Accessors(makeFinal = true, chain = true, fluent = false) private boolean writeOnly; - private Function[], T[]> deferredInit; + private Function[], T[]> deferredInit; private boolean isTotalsValidator = false; @JsonIgnore @@ -59,11 +59,16 @@ public abstract class ValidatableProperty { private Integer thresholdPercentage; // TODO.impl: move to IntegerProperty + public final P self() { + //noinspection unchecked + return (P) this; + } + public String unit() { return null; } - protected void setDeferredInit(final Function[], T[]> function) { +protected void setDeferredInit(final Function[], T[]> function) { this.deferredInit = function; } @@ -71,47 +76,47 @@ public abstract class ValidatableProperty { return deferredInit != null; } - public T[] doDeferredInit(final ValidatableProperty[] allProperties) { + public T[] doDeferredInit(final ValidatableProperty[] allProperties) { return deferredInit.apply(allProperties); } - public ValidatableProperty writeOnly() { + public P writeOnly() { this.writeOnly = true; optional(); - return this; + return self(); } - public ValidatableProperty readOnly() { + public P readOnly() { this.readOnly = true; optional(); - return this; + return self(); } - public ValidatableProperty required() { + public P required() { required = TRUE; - return this; + return self(); } - public ValidatableProperty optional() { + public ValidatableProperty optional() { required = FALSE; return this; } - public ValidatableProperty withDefault(final T value) { + public P withDefault(final T value) { defaultValue = value; required = FALSE; - return this; + return self(); } - public void deferredInit(final ValidatableProperty[] allProperties) { + public void deferredInit(final ValidatableProperty[] allProperties) { } - public ValidatableProperty asTotalLimit() { + public P asTotalLimit() { isTotalsValidator = true; - return this; + return self(); } - public ValidatableProperty asTotalLimitFor(final String propertyName, final String propertyValue) { + public P asTotalLimitFor(final String propertyName, final String propertyValue) { if (asTotalLimitValidators == null) { asTotalLimitValidators = new ArrayList<>(); } @@ -132,7 +137,7 @@ public abstract class ValidatableProperty { return emptyList(); }; asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1)); - return this; + return self(); } public String propertyName() { @@ -147,7 +152,7 @@ public abstract class ValidatableProperty { return thresholdPercentage; } - public ValidatableProperty eachComprising(final int factor, final TriFunction> validator) { + public ValidatableProperty eachComprising(final int factor, final TriFunction> validator) { if (asTotalLimitValidators == null) { asTotalLimitValidators = new ArrayList<>(); } @@ -155,9 +160,9 @@ public abstract class ValidatableProperty { return this; } - public ValidatableProperty withThreshold(final Integer percentage) { + public P withThreshold(final Integer percentage) { this.thresholdPercentage = percentage; - return this; + return self(); } public final List validate(final PropertiesProvider propsProvider) { @@ -250,10 +255,10 @@ public abstract class ValidatableProperty { .toList(); } - public ValidatableProperty computedBy(final Function compute) { + public P computedBy(final Function compute) { this.computedBy = compute; this.computed = true; - return this; + return self(); } public T compute(final E entity) { diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java index 86a4766a..57e76381 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java @@ -6,6 +6,8 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import static java.util.Arrays.asList; + /** * Java has List.of(...), Set.of(...) and Map.of(...) all with varargs parameter, * but no Array.of(...). Here it is. @@ -48,4 +50,17 @@ public class Array { public static T[] emptyArray() { return of(); } + + public static T[] insertAfterEntry(final T[] array, final T entryToFind, final T newEntry) { + final var arrayList = new ArrayList<>(asList(array)); + final var index = arrayList.indexOf(entryToFind); + if (index < 0) { + throw new IllegalArgumentException("entry "+ entryToFind + " not found in " + Arrays.toString(array)); + } + arrayList.add(index + 1, newEntry); + + @SuppressWarnings("unchecked") + final var extendedArray = (T[]) java.lang.reflect.Array.newInstance(array.getClass().getComponentType(), array.length); + return arrayList.toArray(extendedArray); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java new file mode 100644 index 00000000..6fc39578 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java @@ -0,0 +1,41 @@ +package net.hostsharing.hsadminng.hash; + +import org.junit.jupiter.api.Test; + +import java.util.Base64; + +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 OTHER_PASSWORD = "other password"; + final String GIVEN_PASSWORD = "given password"; + final String GIVEN_PASSWORD_HASH = "foKDNQP0oZo0pjFpss5vNl0kfHOs6MKMaJUUbpJTg6hqI1WY+KbU/PKQIg2xt/mwDMmW5WR0pdUZnTv8RPTfhjprZUNqTXJsUXczQnczYUxE"; + final String GIVEN_SALT = "given salt"; + + @Test + void verifiesHashedPasswordWithRandomSalt() { + final var hash = hashAlgorithm(SHA512).withRandomSalt().generate(GIVEN_PASSWORD); + hashAlgorithm(SHA512).withHash(hash).verify(GIVEN_PASSWORD); // throws exception if wrong + } + + @Test + void verifiesHashedPasswordWithGivenSalt() { + final var hash = hashAlgorithm(SHA512).withSalt(GIVEN_SALT).generate(GIVEN_PASSWORD); + + final var decoded = new String(Base64.getDecoder().decode(hash)); + assertThat(decoded).endsWith(":" + GIVEN_SALT); + hashAlgorithm(SHA512).withHash(hash).verify(GIVEN_PASSWORD); // throws exception if wrong + } + + @Test + void throwsExceptionForInvalidPassword() { + final var throwable = catchThrowable(() -> + hashAlgorithm(SHA512).withHash(GIVEN_PASSWORD_HASH).verify(OTHER_PASSWORD)); + + assertThat(throwable).hasMessage("invalid password"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index 8ed76743..2c92d69b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -125,7 +125,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { "{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}", "{type=string, propertyName=homedir, readOnly=true, computed=true}", "{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, undisclosed=true}", - "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, undisclosed=true}" + "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, hashedUsing=SHA512, undisclosed=true}" ); } } 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 66da5f2d..b694c304 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java @@ -6,13 +6,18 @@ import org.junit.jupiter.params.provider.ValueSource; 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.hs.validation.PasswordProperty.passwordProperty; +import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry; import static org.assertj.core.api.Assertions.assertThat; class PasswordPropertyUnitTest { - private final ValidatableProperty passwordProp = passwordProperty("password").minLength(8).maxLength(40).writeOnly(); + private final ValidatableProperty passwordProp = + passwordProperty("password").minLength(8).maxLength(40).hashedUsing(SHA512).writeOnly(); private final List violations = new ArrayList<>(); @ParameterizedTest @@ -89,4 +94,27 @@ class PasswordPropertyUnitTest { .contains("password' must not contain colon (':')") .doesNotContain(givenPassword); } + + @Test + void shouldComputeHash() { + + // when + final var result = passwordProp.compute(new PropertiesProvider() { + + @Override + public Map directProps() { + return Map.ofEntries( + entry(passwordProp.propertyName, "some password") + ); + } + + @Override + public Object getContextValue(final String propName) { + return null; + } + }); + + // then + hashAlgorithm(SHA512).withHash(result).verify("some password"); // throws exception if wrong + } }