Compare commits

...

4 Commits

Author SHA1 Message Date
Michael Hoennig
a1600a1c56 use JNA + C-library-function crypt to create hash 2024-07-01 14:40:53 +02:00
Michael Hoennig
0e9db7e67a rename HashProcessor -> LinuxEtcShadowHashGenerator and cleanup API 2024-07-01 12:35:54 +02:00
Michael Hoennig
3a0c94e42d rename HashProcessor -> EtcShadowHashGenerator and cleanup API 2024-07-01 12:35:37 +02:00
Michael Hoennig
7d7d99c05b revert unnecessary changes 2024-07-01 12:00:30 +02:00
10 changed files with 180 additions and 169 deletions

View File

@ -66,7 +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 'org.bouncycastle:bcpkix-jdk18on:1.76'
implementation 'net.java.dev.jna:jna:5.8.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'

View File

@ -1,113 +0,0 @@
package net.hostsharing.hsadminng.hash;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
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 {
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 COST_FACTOR = 13;
private final Algorithm algorithm;
private String password;
public enum Algorithm {
SHA512("$6$");
final String prefix;
Algorithm(final String prefix) {
this.prefix = prefix;
}
}
private static final String SALT_CHARACTERS =
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789/.";
private String salt;
@SneakyThrows
public static HashProcessor hashAlgorithm(final Algorithm algorithm) {
return new HashProcessor(algorithm);
}
private HashProcessor(final Algorithm algorithm) throws NoSuchAlgorithmException {
this.algorithm = algorithm;
}
public HashProcessor generate(final String password) {
this.password = password;
return this;
}
public String forLinux() {
if (salt == null) {
throw new IllegalStateException("no salt given");
}
if (password == null) {
throw new IllegalStateException("no password given");
}
final var hash = OpenBSDBCrypt.generate(password.toCharArray(), salt.getBytes(), COST_FACTOR);
final var hashedPassword = new String(org.bouncycastle.util.encoders.Base64.encode(hash.getBytes()));
return "$6$" + salt + "$" + hashedPassword;
}
public static void nextSalt(final String salt) {
predefinedSalts.add(salt);
}
public HashProcessor withSalt(final String salt) {
this.salt = salt;
return this;
}
public HashProcessor withRandomSalt() {
if (!predefinedSalts.isEmpty()) {
return withSalt(predefinedSalts.poll());
}
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 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

@ -0,0 +1,115 @@
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[2];
if (parts.length == 5) {
salt += "$" + parts[3];
}
if (!generate().equals(givenHash)) {
throw new IllegalArgumentException("invalid password");
}
}
public String generate() {
if (salt == null) {
throw new IllegalStateException("no salt given");
}
if (plaintextPassword == null) {
throw new IllegalStateException("no password given");
}
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 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 {
NativeCryptLibrary INSTANCE = Native.load("crypt", NativeCryptLibrary.class);
String crypt(String password, String salt);
}
}

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.LinuxEtcShadowHashGenerator;
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(LinuxEtcShadowHashGenerator.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.LinuxEtcShadowHashGenerator.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.LinuxEtcShadowHashGenerator.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

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

@ -0,0 +1,51 @@
package net.hostsharing.hsadminng.hash;
import org.junit.jupiter.api.Test;
import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm.SHA512;
import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
class LinuxEtcShadowHashGeneratorUnitTest {
final String GIVEN_PASSWORD = "given password";
final String WRONG_PASSWORD = "wrong password";
final String GIVEN_SALT = "0123456789abcdef";
// generated via mkpasswd for plaintext password GIVEN_PASSWORD (see above)
final String GIVEN_SHA512_HASH = "$6$ooei1HK6JXVaI7KC$sY5d9fEOr36hjh4CYwIKLMfRKL1539bEmbVCZ.zPiH0sv7jJVnoIXb5YEefEtoSM2WWgDi9hr7vXRe3Nw8zJP/";
final String GIVEN_YESCRYPT_HASH = "$y$j9T$wgYACPmBXvlMg2MzeZA0p1$KXUzd28nG.67GhPnBZ3aZsNNA5bWFdL/dyG4wS0iRw7";
@Test
void verifiesPasswordAgainstSha512HashFromMkpasswd() {
hash(GIVEN_PASSWORD).verify(GIVEN_SHA512_HASH); // throws exception if wrong
}
@Test
void verifiesPasswordAgainstYescryptHashFromMkpasswd() {
hash(GIVEN_PASSWORD).verify(GIVEN_YESCRYPT_HASH); // throws exception if wrong
}
@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

@ -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.LinuxEtcShadowHashGenerator;
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");
LinuxEtcShadowHashGenerator.nextSalt("Jr5w/Y8zo8pCkqg7");
RestAssured // @formatter:off
.given()

View File

@ -68,11 +68,11 @@ class HsCloudServerHostingAssetValidatorUnitTest {
@Test
void validatesBookingItemType() {
// given
final var mangedServerHostingAssetEntity = linkBookingItem(HsHostingAssetEntity.builder()
final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER)
.identifier("xyz00")
.bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM.toBuilder().build())
.build());
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build())
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when
@ -103,9 +103,4 @@ class HsCloudServerHostingAssetValidatorUnitTest {
"'CLOUD_SERVER:xyz00.parentAsset' must be null but is set to D-???????-?:null",
"'CLOUD_SERVER:xyz00.assignedToAsset' must be null but is set to D-???????-?:null");
}
private static HsHostingAssetEntity linkBookingItem(final HsHostingAssetEntity mangedServerHostingAssetEntity) {
mangedServerHostingAssetEntity.getBookingItem().setRelatedHostingAsset(mangedServerHostingAssetEntity);
return mangedServerHostingAssetEntity;
}
}

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.LinuxEtcShadowHashGenerator.Algorithm.SHA512;
import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.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
}
}