implement password-hashing (not fully integrated yet) (#67)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #67
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-06-28 11:00:15 +02:00
parent 6167ef2221
commit 3391ec6cc9
16 changed files with 281 additions and 62 deletions

View File

@ -66,6 +66,7 @@ dependencies {
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
implementation 'org.openapitools:jackson-databind-nullable:0.2.6' implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
implementation 'org.apache.commons:commons-text:1.11.0' 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.modelmapper:modelmapper:3.2.0'
implementation 'org.iban4j:iban4j:3.2.7-RELEASE' implementation 'org.iban4j:iban4j:3.2.7-RELEASE'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'

View File

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

View File

@ -16,7 +16,7 @@ import static java.util.Optional.ofNullable;
public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingItemEntity> { public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingItemEntity> {
public HsBookingItemEntityValidator(final ValidatableProperty<?>... properties) { public HsBookingItemEntityValidator(final ValidatableProperty<?, ?>... properties) {
super(properties); super(properties);
} }
@ -54,7 +54,7 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
// TODO.refa: convert into generic shape like multi-options validator // TODO.refa: convert into generic shape like multi-options validator
private static String validateMaxTotalValue( private static String validateMaxTotalValue(
final HsBookingItemEntity bookingItem, final HsBookingItemEntity bookingItem,
final ValidatableProperty<?> propDef) { final ValidatableProperty<?, ?> propDef) {
final var propName = propDef.propertyName(); final var propName = propDef.propertyName();
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse(""); final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList()) final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList())

View File

@ -24,7 +24,7 @@ import static java.util.Optional.ofNullable;
public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> { public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> {
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.BookingItem bookingItemValidation;
private final HsHostingAssetEntityValidator.ParentAsset parentAssetValidation; private final HsHostingAssetEntityValidator.ParentAsset parentAssetValidation;
@ -36,7 +36,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
@NotNull final ParentAsset parentAssetValidation, @NotNull final ParentAsset parentAssetValidation,
@NotNull final AssignedToAsset assignedToAssetValidation, @NotNull final AssignedToAsset assignedToAssetValidation,
@NotNull final AlarmContact alarmContactValidation, @NotNull final AlarmContact alarmContactValidation,
final ValidatableProperty<?>... properties) { final ValidatableProperty<?, ?>... properties) {
super(properties); super(properties);
this.bookingItemValidation = bookingItemValidation; this.bookingItemValidation = bookingItemValidation;
this.parentAssetValidation = parentAssetValidation; this.parentAssetValidation = parentAssetValidation;
@ -105,7 +105,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
// TODO.test: check, if there are any hosting assets which need this validation at all // TODO.test: check, if there are any hosting assets which need this validation at all
private String validateMaxTotalValue( private String validateMaxTotalValue(
final HsHostingAssetEntity hostingAsset, final HsHostingAssetEntity hostingAsset,
final ValidatableProperty<?> propDef) { final ValidatableProperty<?, ?> propDef) {
final var propName = propDef.propertyName(); final var propName = propDef.propertyName();
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse(""); final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList()) final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators; 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.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
@ -30,7 +31,7 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator {
.withDefault("/bin/false"), .withDefault("/bin/false"),
stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir), stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir),
stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).undisclosed().writeOnly().optional(), 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 @Override

View File

@ -9,7 +9,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
@Setter @Setter
public class BooleanProperty extends ValidatableProperty<Boolean> { public class BooleanProperty extends ValidatableProperty<BooleanProperty, Boolean> {
private static final String[] KEY_ORDER = Array.join(ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_TAIL); 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<Boolean> {
return new BooleanProperty(propertyName); return new BooleanProperty(propertyName);
} }
public ValidatableProperty<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) { public BooleanProperty falseIf(final String refPropertyName, final String refPropertyValue) {
this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue); this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
return this; return this;
} }

View File

@ -9,7 +9,7 @@ import java.util.List;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
@Setter @Setter
public class EnumerationProperty extends ValidatableProperty<String> { public class EnumerationProperty extends ValidatableProperty<EnumerationProperty, String> {
private static final String[] KEY_ORDER = Array.join( private static final String[] KEY_ORDER = Array.join(
ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_HEAD,
@ -26,12 +26,12 @@ public class EnumerationProperty extends ValidatableProperty<String> {
return new EnumerationProperty(propertyName); return new EnumerationProperty(propertyName);
} }
public ValidatableProperty<String> values(final String... values) { public EnumerationProperty values(final String... values) {
this.values = values; this.values = values;
return this; return this;
} }
public void deferredInit(final ValidatableProperty<?>[] allProperties) { public void deferredInit(final ValidatableProperty<?, ?>[] allProperties) {
if (hasDeferredInit()) { if (hasDeferredInit()) {
if (this.values != null) { if (this.values != null) {
throw new IllegalStateException("property " + this + " already has values"); throw new IllegalStateException("property " + this + " already has values");
@ -40,8 +40,8 @@ public class EnumerationProperty extends ValidatableProperty<String> {
} }
} }
public ValidatableProperty<String> valuesFromProperties(final String propertyNamePrefix) { public EnumerationProperty valuesFromProperties(final String propertyNamePrefix) {
this.setDeferredInit( (ValidatableProperty<?>[] allProperties) -> stream(allProperties) this.setDeferredInit( (ValidatableProperty<?, ?>[] allProperties) -> stream(allProperties)
.map(ValidatableProperty::propertyName) .map(ValidatableProperty::propertyName)
.filter(name -> name.startsWith(propertyNamePrefix)) .filter(name -> name.startsWith(propertyNamePrefix))
.map(name -> name.substring(propertyNamePrefix.length())) .map(name -> name.substring(propertyNamePrefix.length()))

View File

@ -14,9 +14,9 @@ import static java.util.Collections.emptyList;
// TODO.refa: rename to HsEntityProcessor, also subclasses // TODO.refa: rename to HsEntityProcessor, also subclasses
public abstract class HsEntityValidator<E extends PropertiesProvider> { public abstract class HsEntityValidator<E extends PropertiesProvider> {
public final ValidatableProperty<?>[] propertyValidators; public final ValidatableProperty<?, ?>[] propertyValidators;
public HsEntityValidator(final ValidatableProperty<?>... validators) { public HsEntityValidator(final ValidatableProperty<?, ?>... validators) {
propertyValidators = validators; propertyValidators = validators;
stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators)); stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators));
} }
@ -68,7 +68,7 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
.orElse(emptyList())); .orElse(emptyList()));
} }
protected static Integer getIntegerValueWithDefault0(final ValidatableProperty<?> prop, final Map<String, Object> propValues) { protected static Integer getIntegerValueWithDefault0(final ValidatableProperty<?, ?> prop, final Map<String, Object> propValues) {
final var value = prop.getValue(propValues); final var value = prop.getValue(propValues);
if (value instanceof Integer) { if (value instanceof Integer) {
return (Integer) value; return (Integer) value;
@ -92,10 +92,10 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
public Map<String, Object> postProcess(final E entity, final Map<String, Object> config) { public Map<String, Object> postProcess(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()) {
if (p.isComputed()) {
copy.put(p.propertyName, p.compute(entity)); copy.put(p.propertyName, p.compute(entity));
} }
}); });

View File

@ -7,7 +7,7 @@ import org.apache.commons.lang3.Validate;
import java.util.List; import java.util.List;
@Setter @Setter
public class IntegerProperty extends ValidatableProperty<Integer> { public class IntegerProperty extends ValidatableProperty<IntegerProperty, Integer> {
private final static String[] KEY_ORDER = Array.join( private final static String[] KEY_ORDER = Array.join(
ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_HEAD,
@ -30,7 +30,7 @@ public class IntegerProperty extends ValidatableProperty<Integer> {
} }
@Override @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(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"); Validate.isTrue(max == null || maxFrom == null, "max and maxFrom are exclusive, but both are given");
} }

View File

@ -1,15 +1,24 @@
package net.hostsharing.hsadminng.hs.validation; package net.hostsharing.hsadminng.hs.validation;
import net.hostsharing.hsadminng.hash.HashProcessor.Algorithm;
import lombok.Setter; import lombok.Setter;
import java.util.List; import java.util.List;
import java.util.stream.Stream; 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 @Setter
public class PasswordProperty extends StringProperty { public class PasswordProperty extends StringProperty<PasswordProperty> {
private static final String[] KEY_ORDER = insertAfterEntry(StringProperty.KEY_ORDER, "computed", "hashedUsing");
private Algorithm hashedUsing;
private PasswordProperty(final String propertyName) { private PasswordProperty(final String propertyName) {
super(propertyName); super(propertyName, KEY_ORDER);
undisclosed(); undisclosed();
} }
@ -23,7 +32,15 @@ public class PasswordProperty extends StringProperty {
validatePassword(result, propValue); 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 @Override
protected String simpleTypeName() { protected String simpleTypeName() {
@ -60,6 +77,5 @@ public class PasswordProperty extends StringProperty {
if (containsColon) { if (containsColon) {
result.add(propertyName + "' must not contain colon (':')"); result.add(propertyName + "' must not contain colon (':')");
} }
} }
} }

View File

@ -6,10 +6,11 @@ import net.hostsharing.hsadminng.mapper.Array;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@Setter
public class StringProperty extends ValidatableProperty<String> {
private static final String[] KEY_ORDER = Array.join( @Setter
public class StringProperty<P extends StringProperty<P>> extends ValidatableProperty<P, String> {
protected static final String[] KEY_ORDER = Array.join(
ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_HEAD,
Array.of("matchesRegEx", "minLength", "maxLength"), Array.of("matchesRegEx", "minLength", "maxLength"),
ValidatableProperty.KEY_ORDER_TAIL, ValidatableProperty.KEY_ORDER_TAIL,
@ -23,23 +24,27 @@ public class StringProperty extends ValidatableProperty<String> {
super(String.class, propertyName, KEY_ORDER); super(String.class, propertyName, KEY_ORDER);
} }
public static StringProperty stringProperty(final String propertyName) { protected StringProperty(final String propertyName, final String[] keyOrder) {
return new StringProperty(propertyName); 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; this.minLength = minLength;
return this; return self();
} }
public StringProperty maxLength(final int maxLength) { public P maxLength(final int maxLength) {
this.maxLength = 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); this.matchesRegEx = Pattern.compile(regExPattern);
return this; return self();
} }
/** /**
@ -47,9 +52,9 @@ public class StringProperty extends ValidatableProperty<String> {
* *
* @return this; * @return this;
*/ */
public StringProperty undisclosed() { public P undisclosed() {
this.undisclosed = true; this.undisclosed = true;
return this; return self();
} }
@Override @Override

View File

@ -25,7 +25,7 @@ import static java.util.Optional.ofNullable;
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
public abstract class ValidatableProperty<T> { public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T> {
protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName"); 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"); protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage");
@ -51,7 +51,7 @@ public abstract class ValidatableProperty<T> {
@Accessors(makeFinal = true, chain = true, fluent = false) @Accessors(makeFinal = true, chain = true, fluent = false)
private boolean writeOnly; private boolean writeOnly;
private Function<ValidatableProperty<?>[], T[]> deferredInit; private Function<ValidatableProperty<?, ?>[], T[]> deferredInit;
private boolean isTotalsValidator = false; private boolean isTotalsValidator = false;
@JsonIgnore @JsonIgnore
@ -59,11 +59,16 @@ public abstract class ValidatableProperty<T> {
private Integer thresholdPercentage; // TODO.impl: move to IntegerProperty private Integer thresholdPercentage; // TODO.impl: move to IntegerProperty
public final P self() {
//noinspection unchecked
return (P) this;
}
public String unit() { public String unit() {
return null; return null;
} }
protected void setDeferredInit(final Function<ValidatableProperty<?>[], T[]> function) { protected void setDeferredInit(final Function<ValidatableProperty<?, ?>[], T[]> function) {
this.deferredInit = function; this.deferredInit = function;
} }
@ -71,47 +76,47 @@ public abstract class ValidatableProperty<T> {
return deferredInit != null; return deferredInit != null;
} }
public T[] doDeferredInit(final ValidatableProperty<?>[] allProperties) { public T[] doDeferredInit(final ValidatableProperty<?, ?>[] allProperties) {
return deferredInit.apply(allProperties); return deferredInit.apply(allProperties);
} }
public ValidatableProperty<T> writeOnly() { public P writeOnly() {
this.writeOnly = true; this.writeOnly = true;
optional(); optional();
return this; return self();
} }
public ValidatableProperty<T> readOnly() { public P readOnly() {
this.readOnly = true; this.readOnly = true;
optional(); optional();
return this; return self();
} }
public ValidatableProperty<T> required() { public P required() {
required = TRUE; required = TRUE;
return this; return self();
} }
public ValidatableProperty<T> optional() { public ValidatableProperty<P, T> optional() {
required = FALSE; required = FALSE;
return this; return this;
} }
public ValidatableProperty<T> withDefault(final T value) { public P withDefault(final T value) {
defaultValue = value; defaultValue = value;
required = FALSE; required = FALSE;
return this; return self();
} }
public void deferredInit(final ValidatableProperty<?>[] allProperties) { public void deferredInit(final ValidatableProperty<?, ?>[] allProperties) {
} }
public ValidatableProperty<T> asTotalLimit() { public P asTotalLimit() {
isTotalsValidator = true; isTotalsValidator = true;
return this; return self();
} }
public ValidatableProperty<T> asTotalLimitFor(final String propertyName, final String propertyValue) { public P asTotalLimitFor(final String propertyName, final String propertyValue) {
if (asTotalLimitValidators == null) { if (asTotalLimitValidators == null) {
asTotalLimitValidators = new ArrayList<>(); asTotalLimitValidators = new ArrayList<>();
} }
@ -132,7 +137,7 @@ public abstract class ValidatableProperty<T> {
return emptyList(); return emptyList();
}; };
asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1)); asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1));
return this; return self();
} }
public String propertyName() { public String propertyName() {
@ -147,7 +152,7 @@ public abstract class ValidatableProperty<T> {
return thresholdPercentage; return thresholdPercentage;
} }
public ValidatableProperty<T> eachComprising(final int factor, final TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> validator) { public ValidatableProperty<P, T> eachComprising(final int factor, final TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> validator) {
if (asTotalLimitValidators == null) { if (asTotalLimitValidators == null) {
asTotalLimitValidators = new ArrayList<>(); asTotalLimitValidators = new ArrayList<>();
} }
@ -155,9 +160,9 @@ public abstract class ValidatableProperty<T> {
return this; return this;
} }
public ValidatableProperty<?> withThreshold(final Integer percentage) { public P withThreshold(final Integer percentage) {
this.thresholdPercentage = percentage; this.thresholdPercentage = percentage;
return this; return self();
} }
public final List<String> validate(final PropertiesProvider propsProvider) { public final List<String> validate(final PropertiesProvider propsProvider) {
@ -250,10 +255,10 @@ public abstract class ValidatableProperty<T> {
.toList(); .toList();
} }
public ValidatableProperty<T> computedBy(final Function<PropertiesProvider, T> compute) { public P computedBy(final Function<PropertiesProvider, T> compute) {
this.computedBy = compute; this.computedBy = compute;
this.computed = true; this.computed = true;
return this; return self();
} }
public <E extends PropertiesProvider> T compute(final E entity) { public <E extends PropertiesProvider> T compute(final E entity) {

View File

@ -6,6 +6,8 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import static java.util.Arrays.asList;
/** /**
* Java has List.of(...), Set.of(...) and Map.of(...) all with varargs parameter, * Java has List.of(...), Set.of(...) and Map.of(...) all with varargs parameter,
* but no Array.of(...). Here it is. * but no Array.of(...). Here it is.
@ -48,4 +50,17 @@ public class Array {
public static <T> T[] emptyArray() { public static <T> T[] emptyArray() {
return of(); return of();
} }
public static <T> 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);
}
} }

View File

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

View File

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

View File

@ -6,13 +6,18 @@ import org.junit.jupiter.params.provider.ValueSource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.hs.validation.PasswordProperty.passwordProperty;
import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
class PasswordPropertyUnitTest { class PasswordPropertyUnitTest {
private final ValidatableProperty<String> passwordProp = passwordProperty("password").minLength(8).maxLength(40).writeOnly(); private final ValidatableProperty<PasswordProperty, String> passwordProp =
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(SHA512).writeOnly();
private final List<String> violations = new ArrayList<>(); private final List<String> violations = new ArrayList<>();
@ParameterizedTest @ParameterizedTest
@ -89,4 +94,27 @@ class PasswordPropertyUnitTest {
.contains("password' must not contain colon (':')") .contains("password' must not contain colon (':')")
.doesNotContain(givenPassword); .doesNotContain(givenPassword);
} }
@Test
void shouldComputeHash() {
// when
final var result = passwordProp.compute(new PropertiesProvider() {
@Override
public Map<String, Object> 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
}
} }