diff --git a/build.gradle b/build.gradle index 59ddad7b..63f4a996 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java b/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java index 1fef3c12..24110104 100644 --- a/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java @@ -6,8 +6,8 @@ import java.util.PriorityQueue; import java.util.Queue; import java.util.random.RandomGenerator; -import org.bouncycastle.crypto.generators.OpenBSDBCrypt; - +import com.sun.jna.Library; +import com.sun.jna.Native; public class LinuxEtcShadowHashGenerator { @@ -15,13 +15,13 @@ public class LinuxEtcShadowHashGenerator { private static final Queue predefinedSalts = new PriorityQueue<>(); public static final int SALT_LENGTH = 16; - public static final int COST_FACTOR = 13; private final String plaintextPassword; private Algorithm algorithm; public enum Algorithm { - SHA512("6"); + SHA512("6"), + YESCRYPT("y"); final String prefix; @@ -57,12 +57,15 @@ public class LinuxEtcShadowHashGenerator { void verify(final String givenHash) { final var parts = givenHash.split("\\$"); - if (parts.length != 4) { - throw new IllegalArgumentException("not a "+algorithm.name()+" Linux hash: " + givenHash); + 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"); @@ -76,9 +79,8 @@ public class LinuxEtcShadowHashGenerator { if (plaintextPassword == null) { throw new IllegalStateException("no password given"); } - 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 "$" + algorithm.prefix + "$" + salt + "$" + hashedPassword; + + return NativeCryptLibrary.INSTANCE.crypt(plaintextPassword, "$" + algorithm.prefix + "$" + salt); } public static void nextSalt(final String salt) { @@ -101,4 +103,13 @@ public class LinuxEtcShadowHashGenerator { } 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); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java index 21dba932..c5abcc08 100644 --- a/src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java @@ -13,6 +13,20 @@ class LinuxEtcShadowHashGeneratorUnitTest { 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();