diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/base/PrivilegeConstants.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/base/PrivilegeConstants.java new file mode 100644 index 000000000..10d3bf97f --- /dev/null +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/base/PrivilegeConstants.java @@ -0,0 +1,9 @@ +package li.strolch.privilege.base; + +public class PrivilegeConstants { + + public static final String DEFAULT_ALGORITHM = "PBKDF2WithHmacSHA512"; + public static final int DEFAULT_KEY_LENGTH = 256; + public static final int DEFAULT_SMALL_ITERATIONS = 10000; + public static final int DEFAULT_ITERATIONS = 200000; +} diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultEncryptionHandler.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultEncryptionHandler.java index adf6ade02..28450b397 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultEncryptionHandler.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultEncryptionHandler.java @@ -15,6 +15,13 @@ */ package li.strolch.privilege.handler; +import static li.strolch.privilege.base.PrivilegeConstants.DEFAULT_ALGORITHM; +import static li.strolch.privilege.base.PrivilegeConstants.DEFAULT_ITERATIONS; +import static li.strolch.privilege.base.PrivilegeConstants.DEFAULT_KEY_LENGTH; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -22,28 +29,24 @@ import java.security.spec.InvalidKeySpecException; import java.text.MessageFormat; import java.util.Map; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import li.strolch.privilege.base.PrivilegeException; +import li.strolch.privilege.helper.Crypt; import li.strolch.privilege.helper.XmlConstants; import li.strolch.utils.helper.StringHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** *

* This default {@link EncryptionHandler} creates tokens using a {@link SecureRandom} object. Hashing is done by using * {@link MessageDigest} and the configured algorithm which is passed in the parameters *

- * + * * Required parameters: * - * + * * @author Robert von Burg */ public class DefaultEncryptionHandler implements EncryptionHandler { @@ -73,6 +76,26 @@ public class DefaultEncryptionHandler implements EncryptionHandler { */ private int keyLength; + @Override + public Crypt newCryptInstance() { + return new Crypt().setAlgorithm(this.algorithm).setIterations(this.iterations).setKeyLength(this.keyLength); + } + + @Override + public String getAlgorithm() { + return this.algorithm; + } + + @Override + public int getIterations() { + return this.iterations; + } + + @Override + public int getKeyLength() { + return this.keyLength; + } + @Override public String nextToken() { byte[] bytes = new byte[32]; @@ -94,8 +117,7 @@ public class DefaultEncryptionHandler implements EncryptionHandler { SecretKeyFactory skf = SecretKeyFactory.getInstance(this.algorithm); PBEKeySpec spec = new PBEKeySpec(password, salt, this.iterations, this.keyLength); SecretKey key = skf.generateSecret(spec); - byte[] res = key.getEncoded(); - return res; + return key.getEncoded(); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw new IllegalStateException(e); @@ -108,14 +130,17 @@ public class DefaultEncryptionHandler implements EncryptionHandler { this.secureRandom = new SecureRandom(); // get hash algorithm parameters - this.algorithm = parameterMap.getOrDefault(XmlConstants.XML_PARAM_HASH_ALGORITHM, "PBKDF2WithHmacSHA512"); - this.iterations = Integer.parseInt(parameterMap.getOrDefault(XmlConstants.XML_PARAM_HASH_ITERATIONS, "200000")); - this.keyLength = Integer.parseInt(parameterMap.getOrDefault(XmlConstants.XML_PARAM_HASH_KEY_LENGTH, "256")); + this.algorithm = parameterMap.getOrDefault(XmlConstants.XML_PARAM_HASH_ALGORITHM, DEFAULT_ALGORITHM); + this.iterations = Integer.parseInt( + parameterMap.getOrDefault(XmlConstants.XML_PARAM_HASH_ITERATIONS, String.valueOf(DEFAULT_ITERATIONS))); + this.keyLength = Integer.parseInt( + parameterMap.getOrDefault(XmlConstants.XML_PARAM_HASH_KEY_LENGTH, String.valueOf(DEFAULT_KEY_LENGTH))); // test hash algorithm try { hashPassword("test".toCharArray(), "test".getBytes()); //$NON-NLS-1$ - DefaultEncryptionHandler.logger.info(MessageFormat.format("Using hashing algorithm {0}", this.algorithm)); //$NON-NLS-1$ + DefaultEncryptionHandler.logger + .info(MessageFormat.format("Using hashing algorithm {0}", this.algorithm)); //$NON-NLS-1$ } catch (Exception e) { String msg = "[{0}] Defined parameter {1} is invalid because of underlying exception: {2}"; //$NON-NLS-1$ msg = MessageFormat.format(msg, EncryptionHandler.class.getName(), XmlConstants.XML_PARAM_HASH_ALGORITHM, diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/EncryptionHandler.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/EncryptionHandler.java index aa7feb63d..1294f4da1 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/EncryptionHandler.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/EncryptionHandler.java @@ -17,46 +17,76 @@ package li.strolch.privilege.handler; import java.util.Map; +import li.strolch.privilege.helper.Crypt; + /** * The {@link EncryptionHandler} exposes API which is used to handle encrypting of strings, or returning secure tokens * for certificates and so forth - * + * * @author Robert von Burg */ public interface EncryptionHandler { /** - * Generates a token which can be used to identify certificates and so forth - * - * @return a new token + * Returns a new crypt instance + * + * @return a new crypt instance */ - public String nextToken(); + Crypt newCryptInstance(); + + /** + * Returns the configured algorithm + * + * @return the configured algorithm + */ + String getAlgorithm(); + + /** + * Returns the configured iterations + * + * @return the configured iterations + */ + int getIterations(); + + /** + * Returns the configured key length + * + * @return the configured key length + */ + int getKeyLength(); /** * Generates a token which can be used to identify certificates and so forth - * + * * @return a new token */ - public byte[] nextSalt(); + String nextToken(); + + /** + * Generates a token which can be used to identify certificates and so forth + * + * @return a new token + */ + byte[] nextSalt(); /** * Hashes the given password with the given salt with the configured algorithm - * + * * @param password - * the password + * the password * @param salt - * the salt - * + * the salt + * * @return the hashed password */ - public byte[] hashPassword(final char[] password, final byte[] salt); + byte[] hashPassword(final char[] password, final byte[] salt); /** * Initialize the concrete {@link EncryptionHandler}. The passed parameter map contains any configuration the * concrete {@link EncryptionHandler} might need - * + * * @param parameterMap - * a map containing configuration properties + * a map containing configuration properties */ - public void initialize(Map parameterMap); + void initialize(Map parameterMap); } diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/Crypt.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/Crypt.java new file mode 100644 index 000000000..ad634a9b5 --- /dev/null +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/Crypt.java @@ -0,0 +1,226 @@ +package li.strolch.privilege.helper; + +import static li.strolch.privilege.base.PrivilegeConstants.DEFAULT_SMALL_ITERATIONS; +import static li.strolch.utils.helper.StringHelper.fromHexString; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import li.strolch.utils.dbc.DBC; +import li.strolch.utils.helper.StringHelper; + +public class Crypt { + + private String algorithm; + private int keyLength; + private int iterations; + private byte[] salt; + private byte[] password; + + public Crypt() { + // nothing to do + } + + public String getAlgorithm() { + return algorithm; + } + + public Crypt setAlgorithm(String algorithm) { + this.algorithm = algorithm; + return this; + } + + public byte[] getSalt() { + return salt; + } + + public Crypt setSalt(byte[] salt) { + this.salt = salt; + return this; + } + + public int getKeyLength() { + return this.keyLength; + } + + public Crypt setKeyLength(int keyLength) { + this.keyLength = keyLength; + return this; + } + + public int getIterations() { + return this.iterations; + } + + public Crypt setIterations(int iterations) { + this.iterations = iterations; + return this; + } + + public byte[] getPassword() { + return password; + } + + public Crypt setPassword(byte[] password) { + this.password = password; + return this; + } + + public Crypt parseCrypt(String crypt) { + DBC.PRE.assertNotEmpty("crypt can no be empty", crypt); + + if (crypt.contains("$")) { + String[] parts = crypt.split("\\$"); + + if (parts.length == 5) { + + setAlgorithm(parts[1], true); + + Map algOptions = parseAlgOptions(parts[2]); + if (algOptions == null) + this.iterations = DEFAULT_SMALL_ITERATIONS; + else + this.iterations = Integer.parseInt(algOptions.get("rounds")); + + this.salt = fromHexString(parts[3]); + this.password = fromHexString(parts[4]); + + } else if (parts.length == 4) { + + setAlgorithm(parts[1], true); + this.iterations = DEFAULT_SMALL_ITERATIONS; + this.salt = fromHexString(parts[2]); + this.password = fromHexString(parts[3]); + + } else if (parts.length == 3) { + + setAlgorithm(parts[1], false); + this.password = fromHexString(parts[2]); + + } else { + throw new IllegalStateException("Wrong number of $ chars in " + crypt + ": " + parts.length); + } + + } else { + + this.algorithm = "SHA-512"; + this.password = fromHexString(crypt); + } + + return this; + } + + public void assertSame(char[] password) { + if (!isSame(password)) + throw new IllegalStateException("Passwords not the same"); + } + + public boolean isSame(char[] password) { + if (this.password == null) + throw new IllegalStateException("password not set, call parseCrypt() first!"); + if (password == null) + throw new IllegalStateException("password must not be null"); + + try { + + byte[] hash; + if (this.salt == null) { + + hash = StringHelper.hash(this.algorithm, new String(password).getBytes()); + + } else { + + PBEKeySpec spec = new PBEKeySpec(password, this.salt, this.iterations, this.keyLength); + SecretKeyFactory skf = SecretKeyFactory.getInstance(this.algorithm); + SecretKey key = skf.generateSecret(spec); + hash = key.getEncoded(); + } + + return Arrays.equals(hash, this.password); + + } catch (Exception e) { + throw new IllegalStateException("Failed validation password for algorithm " + this.algorithm, e); + } + } + + public String toCryptString() { + + StringBuilder sb = new StringBuilder(); + + sb.append("$"); + switch (this.algorithm) { + case "MD5": + sb.append("1"); + break; + + case "PBKDF2WithHmacSHA256": + case "SHA-256": + sb.append("5"); + break; + + case "PBKDF2WithHmacSHA512": + case "SHA-512": + sb.append("6"); + break; + + default: + throw new IllegalStateException("Unhandled algorithm " + this.algorithm); + } + + if (this.iterations != 0 && this.iterations != DEFAULT_SMALL_ITERATIONS) { + sb.append("$"); + sb.append("rounds"); + sb.append("="); + sb.append(iterations); + } + + if (this.salt != null) { + sb.append("$"); + sb.append(StringHelper.toHexString(this.salt)); + } + + sb.append("$"); + sb.append(StringHelper.toHexString(this.password)); + + return sb.toString(); + } + + private Map parseAlgOptions(String part) { + String[] options = part.split(","); + Map algOptions = new HashMap<>(options.length); + for (String option : options) { + if (option.trim().isEmpty()) + continue; + if (!option.contains("=")) + throw new IllegalStateException("Option " + option + " is missing = char"); + String[] keyValue = option.split("="); + algOptions.put(keyValue[0].trim(), keyValue[1].trim()); + } + + return algOptions; + } + + private void setAlgorithm(String id, boolean hasSalt) { + switch (id) { + case "1": + this.algorithm = "MD5"; + this.keyLength = 0; + break; + case "5": + this.algorithm = hasSalt ? "PBKDF2WithHmacSHA256" : "SHA-256"; + this.keyLength = 256; + break; + case "6": + this.algorithm = hasSalt ? "PBKDF2WithHmacSHA512" : "SHA-512"; + this.keyLength = 256; + break; + + default: + throw new IllegalStateException("Unhandled ID " + id); + } + } +} diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/PasswordCreator.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/PasswordCreator.java index fbab33506..b17a938a9 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/PasswordCreator.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/PasswordCreator.java @@ -15,13 +15,16 @@ */ package li.strolch.privilege.helper; +import static li.strolch.privilege.base.PrivilegeConstants.DEFAULT_ALGORITHM; +import static li.strolch.privilege.base.PrivilegeConstants.DEFAULT_SMALL_ITERATIONS; +import static li.strolch.privilege.base.PrivilegeConstants.DEFAULT_KEY_LENGTH; + +import javax.crypto.SecretKeyFactory; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; -import javax.crypto.SecretKeyFactory; - import li.strolch.privilege.handler.DefaultEncryptionHandler; import li.strolch.utils.helper.StringHelper; @@ -29,16 +32,17 @@ import li.strolch.utils.helper.StringHelper; *

* Simple main class which can be used to create a hash from a password which the user must type in at the command line *

- * + * * @author Robert von Burg */ public class PasswordCreator { /** * @param args - * the args from the command line, NOT USED + * the args from the command line, NOT USED + * * @throws Exception - * thrown if anything goes wrong + * thrown if anything goes wrong */ @SuppressWarnings("nls") public static void main(String[] args) throws Exception { @@ -49,11 +53,11 @@ public class PasswordCreator { String hashAlgorithm = null; while (hashAlgorithm == null) { - System.out.print("Hash Algorithm [PBKDF2WithHmacSHA512]: "); + System.out.print("Hash Algorithm [" + DEFAULT_ALGORITHM + "]: "); String readLine = r.readLine().trim(); if (readLine.isEmpty()) { - hashAlgorithm = "PBKDF2WithHmacSHA512"; + hashAlgorithm = DEFAULT_ALGORITHM; } else { try { @@ -68,11 +72,11 @@ public class PasswordCreator { int iterations = -1; while (iterations == -1) { - System.out.print("Hash iterations [10000]: "); + System.out.print("Hash iterations [" + DEFAULT_SMALL_ITERATIONS + "]: "); String readLine = r.readLine().trim(); if (readLine.isEmpty()) { - iterations = 10000; + iterations = DEFAULT_SMALL_ITERATIONS; } else { try { @@ -86,11 +90,11 @@ public class PasswordCreator { int keyLength = -1; while (keyLength == -1) { - System.out.print("Hash keyLength [256]: "); + System.out.print("Hash keyLength [" + DEFAULT_KEY_LENGTH + "]: "); String readLine = r.readLine().trim(); if (readLine.isEmpty()) { - keyLength = 256; + keyLength = DEFAULT_KEY_LENGTH; } else { try { @@ -98,6 +102,7 @@ public class PasswordCreator { if (keyLength <= 0) throw new IllegalArgumentException("KeyLength must be > 0"); } catch (Exception e) { + System.err.println(e.getLocalizedMessage()); System.err.println(e.getLocalizedMessage()); keyLength = -1; } @@ -128,8 +133,9 @@ public class PasswordCreator { System.out.println("Salt is: " + saltS); System.out.println(); - System.out.println(XmlConstants.XML_ATTR_PASSWORD + "=\"" + passwordHashS + "\" " - + XmlConstants.XML_ATTR_SALT + "=\"" + saltS + "\""); + System.out.println( + XmlConstants.XML_ATTR_PASSWORD + "=\"" + passwordHashS + "\" " + XmlConstants.XML_ATTR_SALT + "=\"" + + saltS + "\""); System.out.println(); } } diff --git a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/CryptTest.java b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/CryptTest.java new file mode 100644 index 000000000..e1ec74613 --- /dev/null +++ b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/CryptTest.java @@ -0,0 +1,100 @@ +package li.strolch.privilege.test; + +import static li.strolch.privilege.base.PrivilegeConstants.*; +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +import li.strolch.privilege.handler.DefaultEncryptionHandler; +import li.strolch.privilege.helper.Crypt; +import li.strolch.privilege.helper.XmlConstants; +import org.junit.BeforeClass; +import org.junit.Test; + +public class CryptTest { + + private static DefaultEncryptionHandler encryptionHandler; + + @BeforeClass + public static void beforeClass() { + Map parameterMap = new HashMap<>(); + parameterMap.put(XmlConstants.XML_PARAM_HASH_ALGORITHM, DEFAULT_ALGORITHM); + parameterMap.put(XmlConstants.XML_PARAM_HASH_ITERATIONS, "" + DEFAULT_SMALL_ITERATIONS); + parameterMap.put(XmlConstants.XML_PARAM_HASH_KEY_LENGTH, "" + DEFAULT_KEY_LENGTH); + + encryptionHandler = new DefaultEncryptionHandler(); + encryptionHandler.initialize(parameterMap); + } + + @Test + public void shouldAssertSamePassword01() { + + String hash = "$1$21232f297a57a5a743894a0e4a801fc3"; + char[] password = "admin".toCharArray(); + + Crypt crypt = new Crypt().parseCrypt(hash); + crypt.assertSame(password); + assertEquals(hash, crypt.toCryptString()); + } + + @Test + public void shouldAssertSamePassword05() { + + String hash = "$5$8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918"; + char[] password = "admin".toCharArray(); + + Crypt crypt = new Crypt().parseCrypt(hash); + crypt.assertSame(password); + assertEquals(hash, crypt.toCryptString()); + } + + @Test + public void shouldAssertSamePassword06() { + + String hash = "$6$c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec"; + char[] password = "admin".toCharArray(); + + Crypt crypt = new Crypt().parseCrypt(hash); + crypt.assertSame(password); + assertEquals(hash, crypt.toCryptString()); + } + + @Test + public void shouldAssertSamePassword15() { + + String hash = "$5$61646d696e$f4aec2c20dd0c3ff0547f4bd56facd76097cce7c613da80c67842b6a357fdc04"; + char[] password = "admin".toCharArray(); + + Crypt crypt = new Crypt().parseCrypt(hash); + crypt.assertSame(password); + assertEquals(hash, crypt.toCryptString()); + } + + @Test + public void shouldAssertSamePassword16() { + + String hash = "$6$rounds=5000$61646d696e$5a39ca7443147f9bf549ee0c2d5ded0640690ed56ef8c903e1b0da2a3339010b"; + char[] password = "admin".toCharArray(); + + Crypt crypt = new Crypt().parseCrypt(hash); + crypt.assertSame(password); + assertEquals(hash, crypt.toCryptString()); + } + + @Test + public void shouldAssertSamePassword20() { + + char[] password = "admin".toCharArray(); + + byte[] salt = "admin".getBytes(); + byte[] passwordHash = encryptionHandler.hashPassword(password, salt); + + Crypt crypt = encryptionHandler.newCryptInstance(); + crypt.setSalt(salt); + crypt.setPassword(passwordHash); + + String hash = "$6$61646d696e$cb69962946617da006a2f95776d78b49e5ec7941d2bdb2d25cdb05f957f64344"; + assertEquals(hash, crypt.toCryptString()); + } +}