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:
*
* - {@link XmlConstants#XML_PARAM_HASH_ALGORITHM}
*
- *
+ *
* @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());
+ }
+}