integrate-sha512-password-hashing #68
@ -1,17 +1,15 @@
|
||||
package net.hostsharing.hsadminng.hash;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
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 {
|
||||
public class EtcShadowHashGenerator {
|
||||
|
||||
private static final RandomGenerator random = new SecureRandom();
|
||||
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 COST_FACTOR = 13;
|
||||
|
||||
private final Algorithm algorithm;
|
||||
private String password;
|
||||
private final String plaintextPassword;
|
||||
private Algorithm algorithm;
|
||||
|
||||
public enum Algorithm {
|
||||
SHA512("$6$");
|
||||
SHA512("6");
|
||||
|
||||
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 =
|
||||
@ -39,42 +42,55 @@ public class HashProcessor {
|
||||
|
||||
private String salt;
|
||||
|
||||
@SneakyThrows
|
||||
public static HashProcessor hashAlgorithm(final Algorithm algorithm) {
|
||||
return new HashProcessor(algorithm);
|
||||
public static EtcShadowHashGenerator hash(final String plaintextPassword) {
|
||||
return new EtcShadowHashGenerator(plaintextPassword);
|
||||
}
|
||||
|
||||
private HashProcessor(final Algorithm algorithm) throws NoSuchAlgorithmException {
|
||||
this.algorithm = algorithm;
|
||||
private EtcShadowHashGenerator(final String plaintextPassword) {
|
||||
this.plaintextPassword = plaintextPassword;
|
||||
}
|
||||
|
||||
public HashProcessor generate(final String password) {
|
||||
this.password = password;
|
||||
public EtcShadowHashGenerator using(final Algorithm algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
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) {
|
||||
throw new IllegalStateException("no salt given");
|
||||
}
|
||||
if (password == null) {
|
||||
if (plaintextPassword == null) {
|
||||
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()));
|
||||
return "$6$" + salt + "$" + hashedPassword;
|
||||
return "$" + algorithm.prefix + "$" + salt + "$" + hashedPassword;
|
||||
}
|
||||
|
||||
public static void nextSalt(final String salt) {
|
||||
predefinedSalts.add(salt);
|
||||
}
|
||||
|
||||
public HashProcessor withSalt(final String salt) {
|
||||
public EtcShadowHashGenerator withSalt(final String salt) {
|
||||
this.salt = salt;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HashProcessor withRandomSalt() {
|
||||
public EtcShadowHashGenerator withRandomSalt() {
|
||||
if (!predefinedSalts.isEmpty()) {
|
||||
return withSalt(predefinedSalts.poll());
|
||||
}
|
||||
@ -85,29 +101,4 @@ public class HashProcessor {
|
||||
}
|
||||
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;
|
||||
|
||||
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.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(EtcShadowHashGenerator.Algorithm.SHA512).writeOnly());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,13 +1,13 @@
|
||||
package net.hostsharing.hsadminng.hs.validation;
|
||||
|
||||
import net.hostsharing.hsadminng.hash.HashProcessor.Algorithm;
|
||||
import net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.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.EtcShadowHashGenerator.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();
|
||||
}
|
||||
|
@ -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.http.ContentType;
|
||||
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.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");
|
||||
EtcShadowHashGenerator.nextSalt("Jr5w/Y8zo8pCkqg7");
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
|
@ -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.EtcShadowHashGenerator.Algorithm.SHA512;
|
||||
import static net.hostsharing.hsadminng.hash.EtcShadowHashGenerator.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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user