Skip to content

Commit

Permalink
CODEC-252: more extensions to accommodating different Random's
Browse files Browse the repository at this point in the history
  • Loading branch information
Tompkins committed Feb 4, 2019
1 parent fd2d38b commit 9737fb2
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 17 deletions.
44 changes: 30 additions & 14 deletions src/main/java/org/apache/commons/codec/digest/B64.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.ThreadLocalRandom;
import java.util.Random;

/**
* Base64 like method to convert binary bytes into ASCII chars.
Expand Down Expand Up @@ -65,26 +65,42 @@ static void b64from24bit(final byte b2, final byte b1, final byte b0, final int
}
}

/**
* Generates a string of random chars from the B64T set.
* <p>
* The salt is generated with {@link SecureRandom}.
* </p>
*
* @param num Number of chars to generate.
* @return a random salt {@link String}.
*/
static String getRandomSalt(final int num) {
final StringBuilder saltString = new StringBuilder(num);
try {
final SecureRandom current = SecureRandom.getInstance("SHA1PRNG");
for (int i = 1; i <= num; i++) {
saltString.append(B64T.charAt(current.nextInt(B64T.length())));
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
return saltString.toString();
}

/**
* Generates a string of random chars from the B64T set.
* <p>
* The salt is generated with {@link ThreadLocalRandom}.
* The salt is generated with the {@link Random} provided.
* </p>
*
* @param num
* Number of chars to generate.
* @param num Number of chars to generate.
* @param random an instance of {@link Random}.
* @return a random salt {@link String}.
*/
static String getRandomSalt(final int num) {
static String getRandomSalt(final int num, final Random random) {
final StringBuilder saltString = new StringBuilder(num);
ThreadLocal<SecureRandom> secureRandomThreadLocal = new ThreadLocal<SecureRandom>();
try {
secureRandomThreadLocal.set(SecureRandom.getInstance("SHA1PRNG"));
final SecureRandom current = secureRandomThreadLocal.get();
for (int i = 1; i <= num; i++) {
saltString.append(B64T.charAt(current.nextInt(B64T.length())));
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
for (int i = 1; i <= num; i++) {
saltString.append(B64T.charAt(random.nextInt(B64T.length())));
}
return saltString.toString();
}
Expand Down
69 changes: 68 additions & 1 deletion src/main/java/org/apache/commons/codec/digest/Md5Crypt.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -77,6 +78,23 @@ public static String apr1Crypt(final byte[] keyBytes) {
return apr1Crypt(keyBytes, APR1_PREFIX + B64.getRandomSalt(8));
}

/**
* See {@link #apr1Crypt(byte[], String)} for details.
* <p>
* A salt is generated for you using the user provided {@link Random}.
* </p>
*
* @param keyBytes plaintext string to hash.
* @param random an arbitrary {@link Random} for the user's reason.
* @param random the instance of {@link Random} to use for generating the salt. Consider using {@link SecureRandom}
* or {@link ThreadLocalRandom}.
* @throws IllegalArgumentException when a {@link java.security.NoSuchAlgorithmException} is caught. *
* @see #apr1Crypt(byte[], String)
*/
public static String apr1Crypt(final byte[] keyBytes, final Random random) {
return apr1Crypt(keyBytes, APR1_PREFIX + B64.getRandomSalt(8, random));
}

/**
* See {@link #apr1Crypt(String, String)} for details.
* <p>
Expand Down Expand Up @@ -164,6 +182,28 @@ public static String md5Crypt(final byte[] keyBytes) {
return md5Crypt(keyBytes, MD5_PREFIX + B64.getRandomSalt(8));
}

/**
* Generates a libc6 crypt() compatible "$1$" hash value.
* <p>
* See {@link #md5Crypt(byte[], String)} for details.
*</p>
* <p>
* A salt is generated for you using the instance of {@link Random} you supply.
* </p>
* @param keyBytes
* plaintext string to hash.
* @param random
* the instance of {@link Random} to use for generating the salt. Consider using {@link SecureRandom}
* or {@link ThreadLocalRandom}.
* @return the hash value
* @throws IllegalArgumentException
* when a {@link java.security.NoSuchAlgorithmException} is caught.
* @see #md5Crypt(byte[], String)
*/
public static String md5Crypt(final byte[] keyBytes, final Random random) {
return md5Crypt(keyBytes, MD5_PREFIX + B64.getRandomSalt(8, random));
}

/**
* Generates a libc crypt() compatible "$1$" MD5 based hash value.
* <p>
Expand Down Expand Up @@ -207,12 +247,39 @@ public static String md5Crypt(final byte[] keyBytes, final String salt) {
* when a {@link java.security.NoSuchAlgorithmException} is caught.
*/
public static String md5Crypt(final byte[] keyBytes, final String salt, final String prefix) {
return md5Crypt(keyBytes, salt, prefix, new SecureRandom());
}

/**
* Generates a libc6 crypt() "$1$" or Apache htpasswd "$apr1$" hash value.
* <p>
* See {@link Crypt#crypt(String, String)} or {@link #apr1Crypt(String, String)} for details.
* </p>
*
* @param keyBytes
* plaintext string to hash.
* @param salt
* real salt value without prefix or "rounds=". The salt may be null, in which case a salt is generated for
* you using {@link ThreadLocalRandom}; for more secure salts consider using {@link SecureRandom} to
* generate your own salts.
* @param prefix
* salt prefix
* @param random
* the instance of {@link Random} to use for generating the salt. Consider using {@link SecureRandom}
* or {@link ThreadLocalRandom}.
* @return the hash value
* @throws IllegalArgumentException
* if the salt does not match the allowed pattern
* @throws IllegalArgumentException
* when a {@link java.security.NoSuchAlgorithmException} is caught.
*/
public static String md5Crypt(final byte[] keyBytes, final String salt, final String prefix, final Random random) {
final int keyLen = keyBytes.length;

// Extract the real salt from the given string which can be a complete hash string.
String saltString;
if (salt == null) {
saltString = B64.getRandomSalt(8);
saltString = B64.getRandomSalt(8, random);
} else {
final Pattern p = Pattern.compile("^" + prefix.replace("$", "\\$") + "([\\.\\/a-zA-Z0-9]{1,8}).*");
final Matcher m = p.matcher(salt);
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/org/apache/commons/codec/digest/Sha2Crypt.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -114,6 +115,31 @@ public static String sha256Crypt(final byte[] keyBytes, String salt) {
return sha2Crypt(keyBytes, salt, SHA256_PREFIX, SHA256_BLOCKSIZE, MessageDigestAlgorithms.SHA_256);
}

/**
* Generates a libc6 crypt() compatible "$5$" hash value.
* <p>
* See {@link Crypt#crypt(String, String)} for details.
* </p>
* @param keyBytes
* plaintext to hash
* @param salt
* real salt value without prefix or "rounds=".
* @param random
* the instance of {@link Random} to use for generating the salt. Consider using {@link SecureRandom}
* or {@link ThreadLocalRandom}.
* @return complete hash value including salt
* @throws IllegalArgumentException
* if the salt does not match the allowed pattern
* @throws IllegalArgumentException
* when a {@link java.security.NoSuchAlgorithmException} is caught.
*/
public static String sha256Crypt(final byte[] keyBytes, String salt, Random random) {
if (salt == null) {
salt = SHA256_PREFIX + B64.getRandomSalt(8, random);
}
return sha2Crypt(keyBytes, salt, SHA256_PREFIX, SHA256_BLOCKSIZE, MessageDigestAlgorithms.SHA_256);
}

/**
* Generates a libc6 crypt() compatible "$5$" or "$6$" SHA2 based hash value.
* <p>
Expand Down Expand Up @@ -558,4 +584,33 @@ public static String sha512Crypt(final byte[] keyBytes, String salt) {
}
return sha2Crypt(keyBytes, salt, SHA512_PREFIX, SHA512_BLOCKSIZE, MessageDigestAlgorithms.SHA_512);
}



/**
* Generates a libc6 crypt() compatible "$6$" hash value.
* <p>
* See {@link Crypt#crypt(String, String)} for details.
* </p>
* @param keyBytes
* plaintext to hash
* @param salt
* real salt value without prefix or "rounds=". The salt may be null, in which case a salt is generated for
* you using {@link ThreadLocalRandom}; for more secure salts consider using {@link SecureRandom} to
* generate your own salts.
* @param random
* the instance of {@link Random} to use for generating the salt. Consider using {@link SecureRandom}
* or {@link ThreadLocalRandom}.
* @return complete hash value including salt
* @throws IllegalArgumentException
* if the salt does not match the allowed pattern
* @throws IllegalArgumentException
* when a {@link java.security.NoSuchAlgorithmException} is caught.
*/
public static String sha512Crypt(final byte[] keyBytes, String salt, final Random random) {
if (salt == null) {
salt = SHA512_PREFIX + B64.getRandomSalt(8, random);
}
return sha2Crypt(keyBytes, salt, SHA512_PREFIX, SHA512_BLOCKSIZE, MessageDigestAlgorithms.SHA_512);
}
}
20 changes: 19 additions & 1 deletion src/test/java/org/apache/commons/codec/digest/Apr1CryptTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import org.apache.commons.codec.Charsets;
import org.junit.Test;

import java.util.concurrent.ThreadLocalRandom;

public class Apr1CryptTest {

@Test
Expand Down Expand Up @@ -55,13 +57,29 @@ public void testApr1CryptBytes() {
assertEquals("$apr1$./$kCwT1pY9qXAJElYG9q1QE1", Md5Crypt.apr1Crypt("t\u00e4st".getBytes(Charsets.ISO_8859_1), "$apr1$./$"));
}

@Test
public void testApr1CryptBytesWithThreadLocalRandom() {
// random salt
final byte[] keyBytes = new byte[] { '!', 'b', 'c', '.' };
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
final String hash = Md5Crypt.apr1Crypt(keyBytes, threadLocalRandom);
assertEquals(hash, Md5Crypt.apr1Crypt("!bc.", hash));

// An empty Bytearray equals an empty String
assertEquals("$apr1$foo$P27KyD1htb4EllIPEYhqi0", Md5Crypt.apr1Crypt(new byte[0], "$apr1$foo"));
// UTF-8 stores \u00e4 "a with diaeresis" as two bytes 0xc3 0xa4.
assertEquals("$apr1$./$EeFrYzWWbmTyGdf4xULYc.", Md5Crypt.apr1Crypt("t\u00e4st", "$apr1$./$"));
// ISO-8859-1 stores "a with diaeresis" as single byte 0xe4.
assertEquals("$apr1$./$kCwT1pY9qXAJElYG9q1QE1", Md5Crypt.apr1Crypt("t\u00e4st".getBytes(Charsets.ISO_8859_1), "$apr1$./$"));
}

@Test
public void testApr1CryptExplicitCall() {
// When explicitly called the prefix is optional
assertEquals("$apr1$1234$mAlH7FRST6FiRZ.kcYL.j1", Md5Crypt.apr1Crypt("secret", "1234"));
// When explicitly called without salt, a random one will be used.
assertTrue(Md5Crypt.apr1Crypt("secret".getBytes()).matches("^\\$apr1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
assertTrue(Md5Crypt.apr1Crypt("secret".getBytes(), null).matches("^\\$apr1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
assertTrue(Md5Crypt.apr1Crypt("secret".getBytes(), (String) null).matches("^\\$apr1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
}

@Test
Expand Down
11 changes: 10 additions & 1 deletion src/test/java/org/apache/commons/codec/digest/Md5CryptTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import org.apache.commons.codec.Charsets;
import org.junit.Test;

import java.util.concurrent.ThreadLocalRandom;

public class Md5CryptTest {

@Test
Expand Down Expand Up @@ -56,7 +58,14 @@ public void testMd5CryptBytes() {
@Test
public void testMd5CryptExplicitCall() {
assertTrue(Md5Crypt.md5Crypt("secret".getBytes()).matches("^\\$1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
assertTrue(Md5Crypt.md5Crypt("secret".getBytes(), null).matches("^\\$1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
assertTrue(Md5Crypt.md5Crypt("secret".getBytes(), (String) null).matches("^\\$1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
}

@Test
public void testMd5CryptExplicitCallWithThreadLocalRandom() {
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
assertTrue(Md5Crypt.md5Crypt("secret".getBytes(), threadLocalRandom).matches("^\\$1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
assertTrue(Md5Crypt.md5Crypt("secret".getBytes(), (String) null).matches("^\\$1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
}

@Test
Expand Down
10 changes: 10 additions & 0 deletions src/test/java/org/apache/commons/codec/digest/Sha256CryptTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

import org.apache.commons.codec.Charsets;
import org.junit.Test;
Expand Down Expand Up @@ -57,6 +58,15 @@ public void testSha2CryptRounds() {
assertEquals("$5$rounds=9999$abcd$Rh/8ngVh9oyuS6lL3.fsq.9xbvXJsfyKWxSjO2mPIa7", Sha2Crypt.sha256Crypt("secret".getBytes(Charsets.UTF_8), "$5$rounds=9999$abcd"));
}

@Test
public void testSha2CryptRoundsThreadLocalRandom() {
ThreadLocalRandom random = ThreadLocalRandom.current();
// minimum rounds?
assertEquals("$5$rounds=1000$abcd$b8MCU4GEeZIekOy5ahQ8EWfT330hvYGVeDYkBxXBva.", Sha2Crypt.sha256Crypt("secret".getBytes(Charsets.UTF_8), "$5$rounds=50$abcd$", random));
assertEquals("$5$rounds=1001$abcd$SQsJZs7KXKdd2DtklI3TY3tkD7UYA99RD0FBLm4Sk48", Sha2Crypt.sha256Crypt("secret".getBytes(Charsets.UTF_8), "$5$rounds=1001$abcd$", random));
assertEquals("$5$rounds=9999$abcd$Rh/8ngVh9oyuS6lL3.fsq.9xbvXJsfyKWxSjO2mPIa7", Sha2Crypt.sha256Crypt("secret".getBytes(Charsets.UTF_8), "$5$rounds=9999$abcd", random));
}

@Test
public void testSha256CryptExplicitCall() {
assertTrue(Sha2Crypt.sha256Crypt("secret".getBytes()).matches("^\\$5\\$[a-zA-Z0-9./]{0,16}\\$.{1,}$"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

import org.apache.commons.codec.Charsets;
import org.junit.Ignore;
Expand Down Expand Up @@ -56,6 +57,12 @@ public void testSha512CryptExplicitCall() {
assertTrue(Sha2Crypt.sha512Crypt("secret".getBytes(), null).matches("^\\$6\\$[a-zA-Z0-9./]{0,16}\\$.{1,}$"));
}

@Test
public void testSha512CryptExplicitCallThreadLocalRandom() {
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
assertTrue(Sha2Crypt.sha512Crypt("secret".getBytes(), null, threadLocalRandom).matches("^\\$6\\$[a-zA-Z0-9./]{0,16}\\$.{1,}$"));
}

@Test(expected = NullPointerException.class)
public void testSha512CryptNullData() {
Sha2Crypt.sha512Crypt((byte[]) null);
Expand Down

0 comments on commit 9737fb2

Please sign in to comment.