integrate-sha512-password-hashing #68
@ -1,17 +1,15 @@
|
|||||||
package net.hostsharing.hsadminng.hash;
|
package net.hostsharing.hsadminng.hash;
|
||||||
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.PriorityQueue;
|
import java.util.PriorityQueue;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.random.RandomGenerator;
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
|
import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512;
|
|
||||||
|
|
||||||
public class HashProcessor {
|
public class EtcShadowHashGenerator {
|
||||||
|
|
||||||
private static final RandomGenerator random = new SecureRandom();
|
private static final RandomGenerator random = new SecureRandom();
|
||||||
private static final Queue<String> predefinedSalts = new PriorityQueue<>();
|
private static final Queue<String> predefinedSalts = new PriorityQueue<>();
|
||||||
@ -19,17 +17,22 @@ public class HashProcessor {
|
|||||||
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;
|
||||||
|
|
||||||
private final Algorithm algorithm;
|
private final String plaintextPassword;
|
||||||
private String password;
|
private Algorithm algorithm;
|
||||||
|
|
||||||
public enum Algorithm {
|
public enum Algorithm {
|
||||||
SHA512("$6$");
|
SHA512("6");
|
||||||
|
|
||||||
final String prefix;
|
final String prefix;
|
||||||
|
|
||||||
Algorithm(final String prefix) {
|
Algorithm(final String prefix) {
|
||||||
this.prefix = 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 =
|
private static final String SALT_CHARACTERS =
|
||||||
@ -39,42 +42,55 @@ public class HashProcessor {
|
|||||||
|
|
||||||
private String salt;
|
private String salt;
|
||||||
|
|
||||||
@SneakyThrows
|
public static EtcShadowHashGenerator hash(final String plaintextPassword) {
|
||||||
public static HashProcessor hashAlgorithm(final Algorithm algorithm) {
|
return new EtcShadowHashGenerator(plaintextPassword);
|
||||||
return new HashProcessor(algorithm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HashProcessor(final Algorithm algorithm) throws NoSuchAlgorithmException {
|
private EtcShadowHashGenerator(final String plaintextPassword) {
|
||||||
this.algorithm = algorithm;
|
this.plaintextPassword = plaintextPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HashProcessor generate(final String password) {
|
public EtcShadowHashGenerator using(final Algorithm algorithm) {
|
||||||
this.password = password;
|
this.algorithm = algorithm;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String forLinux() {
|
void verify(final String givenHash) {
|
||||||
|
final var parts = givenHash.split("\\$");
|
||||||
|
if (parts.length != 4) {
|
||||||
|
throw new IllegalArgumentException("not a "+algorithm.name()+" Linux hash: " + givenHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithm = Algorithm.byPrefix(parts[1]);
|
||||||
|
salt = parts[2];
|
||||||
|
|
||||||
|
if (!generate().equals(givenHash)) {
|
||||||
|
throw new IllegalArgumentException("invalid password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generate() {
|
||||||
if (salt == null) {
|
if (salt == null) {
|
||||||
throw new IllegalStateException("no salt given");
|
throw new IllegalStateException("no salt given");
|
||||||
}
|
}
|
||||||
if (password == null) {
|
if (plaintextPassword == null) {
|
||||||
throw new IllegalStateException("no password given");
|
throw new IllegalStateException("no password given");
|
||||||
}
|
}
|
||||||
final var hash = OpenBSDBCrypt.generate(password.toCharArray(), salt.getBytes(), COST_FACTOR);
|
final var hash = OpenBSDBCrypt.generate(plaintextPassword.toCharArray(), salt.getBytes(), COST_FACTOR);
|
||||||
final var hashedPassword = new String(org.bouncycastle.util.encoders.Base64.encode(hash.getBytes()));
|
final var hashedPassword = new String(org.bouncycastle.util.encoders.Base64.encode(hash.getBytes()));
|
||||||
return "$6$" + salt + "$" + hashedPassword;
|
return "$" + algorithm.prefix + "$" + salt + "$" + hashedPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void nextSalt(final String salt) {
|
public static void nextSalt(final String salt) {
|
||||||
predefinedSalts.add(salt);
|
predefinedSalts.add(salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HashProcessor withSalt(final String salt) {
|
public EtcShadowHashGenerator withSalt(final String salt) {
|
||||||
this.salt = salt;
|
this.salt = salt;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HashProcessor withRandomSalt() {
|
public EtcShadowHashGenerator withRandomSalt() {
|
||||||
if (!predefinedSalts.isEmpty()) {
|
if (!predefinedSalts.isEmpty()) {
|
||||||
return withSalt(predefinedSalts.poll());
|
return withSalt(predefinedSalts.poll());
|
||||||
}
|
}
|
||||||
@ -85,29 +101,4 @@ public class HashProcessor {
|
|||||||
}
|
}
|
||||||
return withSalt(stringBuilder.toString());
|
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,6 +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.hash.EtcShadowHashGenerator;
|
||||||
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;
|
||||||
@ -31,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).hashedUsing(HashProcessor.Algorithm.SHA512).writeOnly());
|
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(EtcShadowHashGenerator.Algorithm.SHA512).writeOnly());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
package net.hostsharing.hsadminng.hs.validation;
|
package net.hostsharing.hsadminng.hs.validation;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.hash.HashProcessor.Algorithm;
|
import net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.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 java.util.Optional.ofNullable;
|
||||||
import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm;
|
import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.hash;
|
||||||
import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntry;
|
import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntry;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@ -36,7 +36,7 @@ public class PasswordProperty extends StringProperty<PasswordProperty> {
|
|||||||
this.hashedUsing = algorithm;
|
this.hashedUsing = algorithm;
|
||||||
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 -> hash(password).using(algorithm).withRandomSalt().generate())
|
||||||
.orElse(null));
|
.orElse(null));
|
||||||
return self();
|
return self();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
package net.hostsharing.hsadminng.hash;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.Algorithm.SHA512;
|
||||||
|
import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.hash;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||||
|
|
||||||
|
class EtcShadowHashGeneratorUnitTest {
|
||||||
|
|
||||||
|
final String GIVEN_PASSWORD = "given password";
|
||||||
|
final String WRONG_PASSWORD = "wrong password";
|
||||||
|
final String GIVEN_SALT = "0123456789abcdef";
|
||||||
|
|
||||||
|
@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");
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +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.hash.EtcShadowHashGenerator;
|
||||||
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;
|
||||||
@ -524,7 +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");
|
EtcShadowHashGenerator.nextSalt("Jr5w/Y8zo8pCkqg7");
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
|
@ -8,8 +8,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512;
|
import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.Algorithm.SHA512;
|
||||||
import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm;
|
import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.hash;
|
||||||
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 net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@ -115,6 +115,6 @@ class PasswordPropertyUnitTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// then
|
// then
|
||||||
hashAlgorithm(SHA512).withHash(result).verify("some password"); // throws exception if wrong
|
hash("some password").using(SHA512).withRandomSalt().generate(); // throws exception if wrong
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user