1
0

add-mariadb-instance-database-and-user-validations (#75)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/75
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-07-12 10:54:47 +02:00
parent 9d2692add3
commit 46fce275ae
27 changed files with 813 additions and 226 deletions

View File

@ -0,0 +1,89 @@
package net.hostsharing.hsadminng.hash;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.function.BiFunction;
import java.util.random.RandomGenerator;
import lombok.Getter;
/**
* Usage-example to generate hash:
* HashGenerator.using(LINUX_SHA512).withRandomSalt().hash("plaintext password");
*
* Usage-example to verify hash:
* HashGenerator.fromHash("hashed password).verify("plaintext password");
*/
@Getter
public final class HashGenerator {
private static final RandomGenerator random = new SecureRandom();
private static final Queue<String> predefinedSalts = new PriorityQueue<>();
public static final int RANDOM_SALT_LENGTH = 16;
private static final String RANDOM_SALT_CHARACTERS =
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789/.";
public enum Algorithm {
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y"),
MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*");
final BiFunction<HashGenerator, String, String> implementation;
final String prefix;
Algorithm(BiFunction<HashGenerator, String, String> implementation, final String prefix) {
this.implementation = implementation;
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 final Algorithm algorithm;
private String salt;
public static HashGenerator using(final Algorithm algorithm) {
return new HashGenerator(algorithm);
}
private HashGenerator(final Algorithm algorithm) {
this.algorithm = algorithm;
}
public String hash(final String plaintextPassword) {
if (plaintextPassword == null) {
throw new IllegalStateException("no password given");
}
return algorithm.implementation.apply(this, plaintextPassword);
}
public static void nextSalt(final String salt) {
predefinedSalts.add(salt);
}
public HashGenerator withSalt(final String salt) {
this.salt = salt;
return this;
}
public HashGenerator withRandomSalt() {
if (!predefinedSalts.isEmpty()) {
return withSalt(predefinedSalts.poll());
}
final var stringBuilder = new StringBuilder(RANDOM_SALT_LENGTH);
for (int i = 0; i < RANDOM_SALT_LENGTH; ++i) {
int randomIndex = random.nextInt(RANDOM_SALT_CHARACTERS.length());
stringBuilder.append(RANDOM_SALT_CHARACTERS.charAt(randomIndex));
}
return withSalt(stringBuilder.toString());
}
}

View File

@ -1,107 +1,31 @@
package net.hostsharing.hsadminng.hash;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.random.RandomGenerator;
import com.sun.jna.Library;
import com.sun.jna.Native;
public class LinuxEtcShadowHashGenerator {
private static final RandomGenerator random = new SecureRandom();
private static final Queue<String> predefinedSalts = new PriorityQueue<>();
public static final int SALT_LENGTH = 16;
private final String plaintextPassword;
private Algorithm algorithm;
public enum Algorithm {
SHA512("6"),
YESCRYPT("y");
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 =
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789/.";
private String salt;
public static LinuxEtcShadowHashGenerator hash(final String plaintextPassword) {
return new LinuxEtcShadowHashGenerator(plaintextPassword);
}
private LinuxEtcShadowHashGenerator(final String plaintextPassword) {
this.plaintextPassword = plaintextPassword;
}
public LinuxEtcShadowHashGenerator using(final Algorithm algorithm) {
this.algorithm = algorithm;
return this;
}
void verify(final String givenHash) {
final var parts = givenHash.split("\\$");
if (parts.length < 3 || parts.length > 5) {
throw new IllegalArgumentException("not a " + algorithm.name() + " Linux hash: " + givenHash);
}
algorithm = Algorithm.byPrefix(parts[1]);
salt = parts.length == 4 ? parts[2] : parts[2] + "$" + parts[3];
if (!generate().equals(givenHash)) {
throw new IllegalArgumentException("invalid password");
}
}
public String generate() {
if (salt == null) {
public static String hash(final HashGenerator generator, final String payload) {
if (generator.getSalt() == null) {
throw new IllegalStateException("no salt given");
}
if (plaintextPassword == null) {
throw new IllegalStateException("no password given");
return NativeCryptLibrary.INSTANCE.crypt(payload, "$" + generator.getAlgorithm().prefix + "$" + generator.getSalt());
}
public static void verify(final String givenHash, final String payload) {
final var parts = givenHash.split("\\$");
if (parts.length < 3 || parts.length > 5) {
throw new IllegalArgumentException("hash with unknown hash method: " + givenHash);
}
return NativeCryptLibrary.INSTANCE.crypt(plaintextPassword, "$" + algorithm.prefix + "$" + salt);
}
public static void nextSalt(final String salt) {
predefinedSalts.add(salt);
}
public LinuxEtcShadowHashGenerator withSalt(final String salt) {
this.salt = salt;
return this;
}
public LinuxEtcShadowHashGenerator withRandomSalt() {
if (!predefinedSalts.isEmpty()) {
return withSalt(predefinedSalts.poll());
final var algorithm = HashGenerator.Algorithm.byPrefix(parts[1]);
final var salt = parts.length == 4 ? parts[2] : parts[2] + "$" + parts[3];
final var calcualatedHash = HashGenerator.using(algorithm).withSalt(salt).hash(payload);
if (!calcualatedHash.equals(givenHash)) {
throw new IllegalArgumentException("invalid password");
}
final var stringBuilder = new StringBuilder(SALT_LENGTH);
for (int i = 0; i < SALT_LENGTH; ++i) {
int randomIndex = random.nextInt(SALT_CHARACTERS.length());
stringBuilder.append(SALT_CHARACTERS.charAt(randomIndex));
}
return withSalt(stringBuilder.toString());
}
public static void main(String[] args) {
System.out.println(NativeCryptLibrary.INSTANCE.crypt("given password", "$6$abcdefghijklmno"));
}
public interface NativeCryptLibrary extends Library {

View File

@ -0,0 +1,35 @@
package net.hostsharing.hsadminng.hash;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MySQLNativePasswordHashGenerator {
public static String hash(final HashGenerator generator, final String password) {
// TODO.impl: if a random salt is generated or not should be part of the algorithm definition
// if (generator.getSalt() != null) {
// throw new IllegalStateException("salt not supported");
// }
try {
final var sha1 = MessageDigest.getInstance("SHA-1");
final var firstHash = sha1.digest(password.getBytes());
final var secondHash = sha1.digest(firstHash);
return "*" + bytesToHex(secondHash).toUpperCase();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-1 algorithm not found", e);
}
}
private static String bytesToHex(byte[] bytes) {
final var hexString = new StringBuilder();
for (byte b : bytes) {
final var hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}

View File

@ -100,13 +100,13 @@ public enum HsHostingAssetType implements Node {
MARIADB_USER( // named e.g. xyz00_abc
inGroup("MariaDB"),
requiredParent(MARIADB_INSTANCE),
assignedTo(MANAGED_WEBSPACE)),
requiredParent(MANAGED_WEBSPACE), // thus, the MANAGED_WEBSPACE:Agent becomes RBAC owner
assignedTo(MARIADB_INSTANCE)), // keep in mind: no RBAC grants implied
MARIADB_DATABASE( // named e.g. xyz00_abc
inGroup("MariaDB"),
requiredParent(MANAGED_WEBSPACE), // TODO.spec: or MARIADB_USER?
assignedTo(MARIADB_INSTANCE)), // TODO.spec: or swapping parent+assignedTo?
requiredParent(MARIADB_USER), // thus, the MARIADB_USER:Agent becomes RBAC owner
assignedTo(MARIADB_INSTANCE)), // keep in mind: no RBAC grants implied
IP_NUMBER(
inGroup("Server"),

View File

@ -26,6 +26,9 @@ public class HostingAssetEntityValidatorRegistry {
register(DOMAIN_SMTP_SETUP, new HsDomainSmtpSetupHostingAssetValidator());
register(DOMAIN_MBOX_SETUP, new HsDomainMboxSetupHostingAssetValidator());
register(EMAIL_ADDRESS, new HsEMailAddressHostingAssetValidator());
register(MARIADB_INSTANCE, new HsMariaDbInstanceHostingAssetValidator());
register(MARIADB_USER, new HsMariaDbUserHostingAssetValidator());
register(MARIADB_DATABASE, new HsMariaDbDatabaseHostingAssetValidator());
}
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {

View File

@ -0,0 +1,25 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
class HsMariaDbDatabaseHostingAssetValidator extends HostingAssetEntityValidator {
public HsMariaDbDatabaseHostingAssetValidator() {
super(
MARIADB_DATABASE,
AlarmContact.isOptional(),
stringProperty("encoding").matchesRegEx("[a-z0-9_]+").maxLength(24).provided("latin1", "utf8").withDefault("utf8"));
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
final var webspaceIdentifier = assetEntity.getParentAsset().getParentAsset().getIdentifier();
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$");
}
}

View File

@ -0,0 +1,37 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_INSTANCE;
class HsMariaDbInstanceHostingAssetValidator extends HostingAssetEntityValidator {
final static String DEFAULT_INSTANCE_IDENTIFIER_SUFFIX = "|MariaDB.default"; // TODO.spec: specify instance naming
public HsMariaDbInstanceHostingAssetValidator() {
super(
MARIADB_INSTANCE,
AlarmContact.isOptional(), // hostmaster alert address is implicitly added
NO_EXTRA_PROPERTIES); // TODO.spec: specify instance properties, e.g. installed extensions
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile(
"^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier()
+ DEFAULT_INSTANCE_IDENTIFIER_SUFFIX)
+ "$");
}
@Override
public void preprocessEntity(final HsHostingAssetEntity entity) {
super.preprocessEntity(entity);
if (entity.getIdentifier() == null) {
ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(
pa.getIdentifier() + DEFAULT_INSTANCE_IDENTIFIER_SUFFIX));
}
}
}

View File

@ -0,0 +1,33 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hash.HashGenerator;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER;
import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty;
class HsMariaDbUserHostingAssetValidator extends HostingAssetEntityValidator {
public HsMariaDbUserHostingAssetValidator() {
super(
MARIADB_USER,
AlarmContact.isOptional(),
// TODO.impl: we need to be able to suppress updating of fields etc., something like this:
// withFieldValidation(
// referenceProperty(alarmContact).isOptional(),
// referenceProperty(parentAsset).isWriteOnce(),
// referenceProperty(assignedToAsset).isWriteOnce(),
// );
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(HashGenerator.Algorithm.MYSQL_NATIVE).writeOnly());
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier();
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$");
}
}

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator;
import net.hostsharing.hsadminng.hash.HashGenerator;
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 +30,8 @@ class HsUnixUserHostingAssetValidator extends HostingAssetEntityValidator {
.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(LinuxEtcShadowHashGenerator.Algorithm.SHA512).writeOnly());
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(HashGenerator.Algorithm.LINUX_SHA512).writeOnly());
// TODO.spec: public SSH keys?
}
@Override

View File

@ -1,13 +1,13 @@
package net.hostsharing.hsadminng.hs.validation;
import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm;
import net.hostsharing.hsadminng.hash.HashGenerator;
import net.hostsharing.hsadminng.hash.HashGenerator.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.LinuxEtcShadowHashGenerator.hash;
import static net.hostsharing.hsadminng.mapper.Array.insertNewEntriesAfterExistingEntry;
@Setter
@ -36,7 +36,7 @@ public class PasswordProperty extends StringProperty<PasswordProperty> {
this.hashedUsing = algorithm;
computedBy((entity)
-> ofNullable(entity.getDirectValue(propertyName, String.class))
.map(password -> hash(password).using(algorithm).withRandomSalt().generate())
.map(password -> HashGenerator.using(algorithm).withRandomSalt().hash(password))
.orElse(null));
return self();
}