From bd8664eed46d60c1a2e68d4f6699a141e7c21487 Mon Sep 17 00:00:00 2001 From: Peter Centgraf Date: Wed, 23 May 2018 15:41:03 +0200 Subject: [PATCH] REPO-2017 Implement PBKDF2 as an available hashing function --- .../hippoecm/repository/PasswordHelper.java | 100 +++++++++++++++--- .../repository/PasswordHelperTest.java | 25 +++++ 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/community/repository/connector/src/main/java/org/hippoecm/repository/PasswordHelper.java b/community/repository/connector/src/main/java/org/hippoecm/repository/PasswordHelper.java index 8dd8a63feef..bf0f7e4f221 100644 --- a/community/repository/connector/src/main/java/org/hippoecm/repository/PasswordHelper.java +++ b/community/repository/connector/src/main/java/org/hippoecm/repository/PasswordHelper.java @@ -22,8 +22,13 @@ import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; import java.util.StringTokenizer; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + import org.apache.jackrabbit.util.Base64; import org.mindrot.jbcrypt.BCrypt; @@ -53,6 +58,16 @@ import org.mindrot.jbcrypt.BCrypt; public class PasswordHelper { + /** + * Magic string used to identify the BCrypt algorithm. + */ + public static final String JBCRYPT = "2a"; + + /** + * SecretKeyFactory string for the PBKDF2 recommended algorithm. + */ + public static final String PBKDF2 = "PBKDF2WithHmacSHA256"; + /** * Algorithm to use for random number generation */ @@ -61,18 +76,20 @@ public class PasswordHelper { /** * The number of digest iterations is fixed */ - private static final int DIGEST_ITERATIONS = 1039; + private static final int DEFAULT_DIGEST_ITERATIONS = 1039; /** * Use fixed encoding before digesting */ private static final String FIXED_ENCODING = "UTF-8"; - private static final char SEPARATOR = '$'; - public static final String JBCRYPT = "2a"; + /** + * Separator for algorithm, params, and raw hash value. + */ + private static final char SEPARATOR = '$'; /** - * This (default) size of the salt + * This (default) size of the salt, in bytes */ private static int saltSize = 8; @@ -81,8 +98,16 @@ public class PasswordHelper { */ private static String hashingAlgorithm = "SHA-256"; + /** + * Should an existing hash be upgraded to the current algorithm if it uses a different algorithm? + */ private static boolean upgradeToCurrentAlgorithm = true; + /** + * The number of iterations to use for iterative hashing algorithms. + */ + private static int iterations = DEFAULT_DIGEST_ITERATIONS; + /** * Prevent instances of this class */ @@ -156,7 +181,7 @@ public class PasswordHelper { byte[] digest = md.digest(new String(plainText).getBytes(FIXED_ENCODING)); // iterating - for (int i = 0; i < DIGEST_ITERATIONS; i++) { + for (int i = 0; i < DEFAULT_DIGEST_ITERATIONS; i++) { md.reset(); digest = md.digest(digest); } @@ -184,12 +209,15 @@ public class PasswordHelper { // BCrypt isn't a standard JRE MessageDigest implementation, so we need a new code path final SecureRandom random = SecureRandom.getInstance(randomAlgorithm); final String salt = BCrypt.gensalt(getSaltSize(), random); - final String hash = BCrypt.hashpw(String.valueOf(plainText), salt); // BCrypt embeds the salt in the hash and uses the salt position for the work factor // if we recognize jbcrypt by the algorithm version string "2a", we can avoid any special // encoding of the result from hashpw() - return hash; + return BCrypt.hashpw(String.valueOf(plainText), salt); + } + else if (hashingAlgorithm.equals(PBKDF2)) { + byte[] salt = getSalt(); + return buildPBKHashString(salt, iterations, plainText); } else { // we assume that all other algorithms can use a common code path via MessageDigest lookup @@ -198,6 +226,18 @@ public class PasswordHelper { } } + private static String buildPBKHashString(final byte[] salt, final int iterations, final char[] plainText) throws NoSuchAlgorithmException, IOException { + try { + final SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBKDF2); + final SecretKey secretKey = keyFactory.generateSecret(new PBEKeySpec(plainText, salt, iterations, saltSize * 8)); + return SEPARATOR + PBKDF2 + SEPARATOR + byteToBase64(salt) + SEPARATOR + iterations + + SEPARATOR + byteToBase64(secretKey.getEncoded()); + } catch (InvalidKeySpecException e) { + // catch and rethrow to avoid backwards-incompatible method signature change + throw new RuntimeException(e); + } + } + /** * Helper function to build the hash sting in the correct format * @param algorithm @@ -236,31 +276,44 @@ public class PasswordHelper { StringTokenizer st = new StringTokenizer(hash, String.valueOf(SEPARATOR)); int tokens = st.countTokens(); - // invalid encrypted string - if (tokens != 1 && tokens != 3) { - throw new IllegalArgumentException("Invalid hash: " + hash); - } - // plain text hash if (tokens == 1) { return hash.equals(new String(password)); } - // check encrypted hash - if (st.countTokens() == 3) { - String algorithm = st.nextToken(); + String algorithm = st.nextToken(); + final boolean isPBKDF2 = algorithm.equals(PBKDF2); + if ((tokens != 4 && isPBKDF2) + || (tokens != 3) && !isPBKDF2) { + // invalid encrypted string + throw new IllegalArgumentException("Invalid hash: " + hash); + } + + if (st.countTokens() > 1) { + // check encrypted hash String salt = st.nextToken(); if (algorithm.equals(JBCRYPT)) { // BCrypt has a distinct code path for hash checks return BCrypt.checkpw(String.valueOf(password), hash); } + else if (algorithm.equals(PBKDF2)) { + try { + // PBKDF2 uses SecretKeyFactory, not MessageDigest + String newHash = buildPBKHashString(base64ToByte(salt), Integer.parseInt(st.nextToken()), password); + return hash.equals(newHash); + } catch (IOException e) { + // TODO: why return false instead of rethrowing exception? + return false; + } + } else { // otherwise, use the standard MessageDigest hash check try { String newHash = buildHashString(algorithm, password, base64ToByte(salt)); return hash.equals(newHash); } catch (IOException e) { + // TODO: why return false instead of rethrowing exception? return false; } } @@ -321,7 +374,7 @@ public class PasswordHelper { } /** - * Get salt size (or work factor for jBCrypt). + * Get salt size (or work factor for jBCrypt, or salt and key size). * @return int */ public static int getSaltSize() { @@ -330,11 +383,24 @@ public class PasswordHelper { /** * Set salt size for generating new hashes. For MessageDigest-based algorithms, this is a size in bytes. - * For jBCrypt, this is a work factor. + * For jBCrypt, this is a work factor. For PBKDF2, this is also used for the key length. * @param saltSize */ public static void setSaltSize(int saltSize) { PasswordHelper.saltSize = saltSize; } + /** + * Get the number of iterations used for iterative hashing algorithms. + */ + public static int getIterations() { + return PasswordHelper.iterations; + } + + /** + * Set the number of iterations used for iterative hashing algorithms. + */ + public static void setIterations(int iterations) { + PasswordHelper.iterations = iterations; + } } diff --git a/community/repository/connector/src/test/java/org/hippoecm/repository/PasswordHelperTest.java b/community/repository/connector/src/test/java/org/hippoecm/repository/PasswordHelperTest.java index b614559b90a..254567b084d 100644 --- a/community/repository/connector/src/test/java/org/hippoecm/repository/PasswordHelperTest.java +++ b/community/repository/connector/src/test/java/org/hippoecm/repository/PasswordHelperTest.java @@ -16,6 +16,8 @@ package org.hippoecm.repository; import junit.framework.Assert; + +import org.apache.commons.lang.time.StopWatch; import org.junit.Test; import java.io.IOException; @@ -46,6 +48,29 @@ public class PasswordHelperTest { Assert.assertTrue("hash is not matched",PasswordHelper.checkHash(password.toCharArray(), passwordHash)); } + @Test + public void testCheckPBKDF2() throws IOException, NoSuchAlgorithmException { + final String oldAlgorithm = PasswordHelper.getHashingAlgorithm(); + final int oldSaltSize = PasswordHelper.getSaltSize(); +// StopWatch stopWatch = new StopWatch(); +// stopWatch.start(); +// for (int i = 0; i < 100; i++) { + try { + PasswordHelper.setHashingAlgorithm(PasswordHelper.PBKDF2); + PasswordHelper.setSaltSize(20); + String password = "Password2016*#$"; + String passwordHash = PasswordHelper.getHash(password.toCharArray()); +// System.out.println("Hash: " + passwordHash); + Assert.assertTrue("hash is not matched", PasswordHelper.checkHash(password.toCharArray(), passwordHash)); + } finally { + PasswordHelper.setHashingAlgorithm(oldAlgorithm); + PasswordHelper.setSaltSize(oldSaltSize); + } +// } +// stopWatch.stop(); +// System.out.println(stopWatch.toString()); + } + @Test public void testShouldUpgradeAlgorithm() throws IOException, NoSuchAlgorithmException { final boolean oldState = PasswordHelper.getUpgradeToCurrentAlgorithm(); -- 2.24.3 (Apple Git-128)