Skip to content

Commit b80adee

Browse files
committed
Merge branch '1.12' of github.com:chtompki/commons-codec into 1.12
2 parents 700424b + 9737fb2 commit b80adee

7 files changed

Lines changed: 199 additions & 17 deletions

File tree

src/main/java/org/apache/commons/codec/digest/B64.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import java.security.NoSuchAlgorithmException;
2020
import java.security.SecureRandom;
21-
import java.util.concurrent.ThreadLocalRandom;
21+
import java.util.Random;
2222

2323
/**
2424
* Base64 like method to convert binary bytes into ASCII chars.
@@ -65,26 +65,42 @@ static void b64from24bit(final byte b2, final byte b1, final byte b0, final int
6565
}
6666
}
6767

68+
/**
69+
* Generates a string of random chars from the B64T set.
70+
* <p>
71+
* The salt is generated with {@link SecureRandom}.
72+
* </p>
73+
*
74+
* @param num Number of chars to generate.
75+
* @return a random salt {@link String}.
76+
*/
77+
static String getRandomSalt(final int num) {
78+
final StringBuilder saltString = new StringBuilder(num);
79+
try {
80+
final SecureRandom current = SecureRandom.getInstance("SHA1PRNG");
81+
for (int i = 1; i <= num; i++) {
82+
saltString.append(B64T.charAt(current.nextInt(B64T.length())));
83+
}
84+
} catch (NoSuchAlgorithmException e) {
85+
throw new RuntimeException(e);
86+
}
87+
return saltString.toString();
88+
}
89+
6890
/**
6991
* Generates a string of random chars from the B64T set.
7092
* <p>
71-
* The salt is generated with {@link ThreadLocalRandom}.
93+
* The salt is generated with the {@link Random} provided.
7294
* </p>
7395
*
74-
* @param num
75-
* Number of chars to generate.
96+
* @param num Number of chars to generate.
97+
* @param random an instance of {@link Random}.
98+
* @return a random salt {@link String}.
7699
*/
77-
static String getRandomSalt(final int num) {
100+
static String getRandomSalt(final int num, final Random random) {
78101
final StringBuilder saltString = new StringBuilder(num);
79-
ThreadLocal<SecureRandom> secureRandomThreadLocal = new ThreadLocal<SecureRandom>();
80-
try {
81-
secureRandomThreadLocal.set(SecureRandom.getInstance("SHA1PRNG"));
82-
final SecureRandom current = secureRandomThreadLocal.get();
83-
for (int i = 1; i <= num; i++) {
84-
saltString.append(B64T.charAt(current.nextInt(B64T.length())));
85-
}
86-
} catch (NoSuchAlgorithmException e) {
87-
throw new RuntimeException(e);
102+
for (int i = 1; i <= num; i++) {
103+
saltString.append(B64T.charAt(random.nextInt(B64T.length())));
88104
}
89105
return saltString.toString();
90106
}

src/main/java/org/apache/commons/codec/digest/Md5Crypt.java

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.security.MessageDigest;
2020
import java.security.SecureRandom;
2121
import java.util.Arrays;
22+
import java.util.Random;
2223
import java.util.concurrent.ThreadLocalRandom;
2324
import java.util.regex.Matcher;
2425
import java.util.regex.Pattern;
@@ -77,6 +78,23 @@ public static String apr1Crypt(final byte[] keyBytes) {
7778
return apr1Crypt(keyBytes, APR1_PREFIX + B64.getRandomSalt(8));
7879
}
7980

81+
/**
82+
* See {@link #apr1Crypt(byte[], String)} for details.
83+
* <p>
84+
* A salt is generated for you using the user provided {@link Random}.
85+
* </p>
86+
*
87+
* @param keyBytes plaintext string to hash.
88+
* @param random an arbitrary {@link Random} for the user's reason.
89+
* @param random the instance of {@link Random} to use for generating the salt. Consider using {@link SecureRandom}
90+
* or {@link ThreadLocalRandom}.
91+
* @throws IllegalArgumentException when a {@link java.security.NoSuchAlgorithmException} is caught. *
92+
* @see #apr1Crypt(byte[], String)
93+
*/
94+
public static String apr1Crypt(final byte[] keyBytes, final Random random) {
95+
return apr1Crypt(keyBytes, APR1_PREFIX + B64.getRandomSalt(8, random));
96+
}
97+
8098
/**
8199
* See {@link #apr1Crypt(String, String)} for details.
82100
* <p>
@@ -164,6 +182,28 @@ public static String md5Crypt(final byte[] keyBytes) {
164182
return md5Crypt(keyBytes, MD5_PREFIX + B64.getRandomSalt(8));
165183
}
166184

185+
/**
186+
* Generates a libc6 crypt() compatible "$1$" hash value.
187+
* <p>
188+
* See {@link #md5Crypt(byte[], String)} for details.
189+
*</p>
190+
* <p>
191+
* A salt is generated for you using the instance of {@link Random} you supply.
192+
* </p>
193+
* @param keyBytes
194+
* plaintext string to hash.
195+
* @param random
196+
* the instance of {@link Random} to use for generating the salt. Consider using {@link SecureRandom}
197+
* or {@link ThreadLocalRandom}.
198+
* @return the hash value
199+
* @throws IllegalArgumentException
200+
* when a {@link java.security.NoSuchAlgorithmException} is caught.
201+
* @see #md5Crypt(byte[], String)
202+
*/
203+
public static String md5Crypt(final byte[] keyBytes, final Random random) {
204+
return md5Crypt(keyBytes, MD5_PREFIX + B64.getRandomSalt(8, random));
205+
}
206+
167207
/**
168208
* Generates a libc crypt() compatible "$1$" MD5 based hash value.
169209
* <p>
@@ -207,12 +247,39 @@ public static String md5Crypt(final byte[] keyBytes, final String salt) {
207247
* when a {@link java.security.NoSuchAlgorithmException} is caught.
208248
*/
209249
public static String md5Crypt(final byte[] keyBytes, final String salt, final String prefix) {
250+
return md5Crypt(keyBytes, salt, prefix, new SecureRandom());
251+
}
252+
253+
/**
254+
* Generates a libc6 crypt() "$1$" or Apache htpasswd "$apr1$" hash value.
255+
* <p>
256+
* See {@link Crypt#crypt(String, String)} or {@link #apr1Crypt(String, String)} for details.
257+
* </p>
258+
*
259+
* @param keyBytes
260+
* plaintext string to hash.
261+
* @param salt
262+
* real salt value without prefix or "rounds=". The salt may be null, in which case a salt is generated for
263+
* you using {@link ThreadLocalRandom}; for more secure salts consider using {@link SecureRandom} to
264+
* generate your own salts.
265+
* @param prefix
266+
* salt prefix
267+
* @param random
268+
* the instance of {@link Random} to use for generating the salt. Consider using {@link SecureRandom}
269+
* or {@link ThreadLocalRandom}.
270+
* @return the hash value
271+
* @throws IllegalArgumentException
272+
* if the salt does not match the allowed pattern
273+
* @throws IllegalArgumentException
274+
* when a {@link java.security.NoSuchAlgorithmException} is caught.
275+
*/
276+
public static String md5Crypt(final byte[] keyBytes, final String salt, final String prefix, final Random random) {
210277
final int keyLen = keyBytes.length;
211278

212279
// Extract the real salt from the given string which can be a complete hash string.
213280
String saltString;
214281
if (salt == null) {
215-
saltString = B64.getRandomSalt(8);
282+
saltString = B64.getRandomSalt(8, random);
216283
} else {
217284
final Pattern p = Pattern.compile("^" + prefix.replace("$", "\\$") + "([\\.\\/a-zA-Z0-9]{1,8}).*");
218285
final Matcher m = p.matcher(salt);

src/main/java/org/apache/commons/codec/digest/Sha2Crypt.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.security.NoSuchAlgorithmException;
2121
import java.security.SecureRandom;
2222
import java.util.Arrays;
23+
import java.util.Random;
2324
import java.util.concurrent.ThreadLocalRandom;
2425
import java.util.regex.Matcher;
2526
import java.util.regex.Pattern;
@@ -114,6 +115,31 @@ public static String sha256Crypt(final byte[] keyBytes, String salt) {
114115
return sha2Crypt(keyBytes, salt, SHA256_PREFIX, SHA256_BLOCKSIZE, MessageDigestAlgorithms.SHA_256);
115116
}
116117

118+
/**
119+
* Generates a libc6 crypt() compatible "$5$" hash value.
120+
* <p>
121+
* See {@link Crypt#crypt(String, String)} for details.
122+
* </p>
123+
* @param keyBytes
124+
* plaintext to hash
125+
* @param salt
126+
* real salt value without prefix or "rounds=".
127+
* @param random
128+
* the instance of {@link Random} to use for generating the salt. Consider using {@link SecureRandom}
129+
* or {@link ThreadLocalRandom}.
130+
* @return complete hash value including salt
131+
* @throws IllegalArgumentException
132+
* if the salt does not match the allowed pattern
133+
* @throws IllegalArgumentException
134+
* when a {@link java.security.NoSuchAlgorithmException} is caught.
135+
*/
136+
public static String sha256Crypt(final byte[] keyBytes, String salt, Random random) {
137+
if (salt == null) {
138+
salt = SHA256_PREFIX + B64.getRandomSalt(8, random);
139+
}
140+
return sha2Crypt(keyBytes, salt, SHA256_PREFIX, SHA256_BLOCKSIZE, MessageDigestAlgorithms.SHA_256);
141+
}
142+
117143
/**
118144
* Generates a libc6 crypt() compatible "$5$" or "$6$" SHA2 based hash value.
119145
* <p>
@@ -558,4 +584,33 @@ public static String sha512Crypt(final byte[] keyBytes, String salt) {
558584
}
559585
return sha2Crypt(keyBytes, salt, SHA512_PREFIX, SHA512_BLOCKSIZE, MessageDigestAlgorithms.SHA_512);
560586
}
587+
588+
589+
590+
/**
591+
* Generates a libc6 crypt() compatible "$6$" hash value.
592+
* <p>
593+
* See {@link Crypt#crypt(String, String)} for details.
594+
* </p>
595+
* @param keyBytes
596+
* plaintext to hash
597+
* @param salt
598+
* real salt value without prefix or "rounds=". The salt may be null, in which case a salt is generated for
599+
* you using {@link ThreadLocalRandom}; for more secure salts consider using {@link SecureRandom} to
600+
* generate your own salts.
601+
* @param random
602+
* the instance of {@link Random} to use for generating the salt. Consider using {@link SecureRandom}
603+
* or {@link ThreadLocalRandom}.
604+
* @return complete hash value including salt
605+
* @throws IllegalArgumentException
606+
* if the salt does not match the allowed pattern
607+
* @throws IllegalArgumentException
608+
* when a {@link java.security.NoSuchAlgorithmException} is caught.
609+
*/
610+
public static String sha512Crypt(final byte[] keyBytes, String salt, final Random random) {
611+
if (salt == null) {
612+
salt = SHA512_PREFIX + B64.getRandomSalt(8, random);
613+
}
614+
return sha2Crypt(keyBytes, salt, SHA512_PREFIX, SHA512_BLOCKSIZE, MessageDigestAlgorithms.SHA_512);
615+
}
561616
}

src/test/java/org/apache/commons/codec/digest/Apr1CryptTest.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.apache.commons.codec.Charsets;
2424
import org.junit.Test;
2525

26+
import java.util.concurrent.ThreadLocalRandom;
27+
2628
public class Apr1CryptTest {
2729

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

60+
@Test
61+
public void testApr1CryptBytesWithThreadLocalRandom() {
62+
// random salt
63+
final byte[] keyBytes = new byte[] { '!', 'b', 'c', '.' };
64+
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
65+
final String hash = Md5Crypt.apr1Crypt(keyBytes, threadLocalRandom);
66+
assertEquals(hash, Md5Crypt.apr1Crypt("!bc.", hash));
67+
68+
// An empty Bytearray equals an empty String
69+
assertEquals("$apr1$foo$P27KyD1htb4EllIPEYhqi0", Md5Crypt.apr1Crypt(new byte[0], "$apr1$foo"));
70+
// UTF-8 stores \u00e4 "a with diaeresis" as two bytes 0xc3 0xa4.
71+
assertEquals("$apr1$./$EeFrYzWWbmTyGdf4xULYc.", Md5Crypt.apr1Crypt("t\u00e4st", "$apr1$./$"));
72+
// ISO-8859-1 stores "a with diaeresis" as single byte 0xe4.
73+
assertEquals("$apr1$./$kCwT1pY9qXAJElYG9q1QE1", Md5Crypt.apr1Crypt("t\u00e4st".getBytes(Charsets.ISO_8859_1), "$apr1$./$"));
74+
}
75+
5876
@Test
5977
public void testApr1CryptExplicitCall() {
6078
// When explicitly called the prefix is optional
6179
assertEquals("$apr1$1234$mAlH7FRST6FiRZ.kcYL.j1", Md5Crypt.apr1Crypt("secret", "1234"));
6280
// When explicitly called without salt, a random one will be used.
6381
assertTrue(Md5Crypt.apr1Crypt("secret".getBytes()).matches("^\\$apr1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
64-
assertTrue(Md5Crypt.apr1Crypt("secret".getBytes(), null).matches("^\\$apr1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
82+
assertTrue(Md5Crypt.apr1Crypt("secret".getBytes(), (String) null).matches("^\\$apr1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
6583
}
6684

6785
@Test

src/test/java/org/apache/commons/codec/digest/Md5CryptTest.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.apache.commons.codec.Charsets;
2424
import org.junit.Test;
2525

26+
import java.util.concurrent.ThreadLocalRandom;
27+
2628
public class Md5CryptTest {
2729

2830
@Test
@@ -56,7 +58,14 @@ public void testMd5CryptBytes() {
5658
@Test
5759
public void testMd5CryptExplicitCall() {
5860
assertTrue(Md5Crypt.md5Crypt("secret".getBytes()).matches("^\\$1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
59-
assertTrue(Md5Crypt.md5Crypt("secret".getBytes(), null).matches("^\\$1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
61+
assertTrue(Md5Crypt.md5Crypt("secret".getBytes(), (String) null).matches("^\\$1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
62+
}
63+
64+
@Test
65+
public void testMd5CryptExplicitCallWithThreadLocalRandom() {
66+
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
67+
assertTrue(Md5Crypt.md5Crypt("secret".getBytes(), threadLocalRandom).matches("^\\$1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
68+
assertTrue(Md5Crypt.md5Crypt("secret".getBytes(), (String) null).matches("^\\$1\\$[a-zA-Z0-9./]{0,8}\\$.{1,}$"));
6069
}
6170

6271
@Test

src/test/java/org/apache/commons/codec/digest/Sha256CryptTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.junit.Assert.assertTrue;
2121

2222
import java.util.Arrays;
23+
import java.util.concurrent.ThreadLocalRandom;
2324

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

61+
@Test
62+
public void testSha2CryptRoundsThreadLocalRandom() {
63+
ThreadLocalRandom random = ThreadLocalRandom.current();
64+
// minimum rounds?
65+
assertEquals("$5$rounds=1000$abcd$b8MCU4GEeZIekOy5ahQ8EWfT330hvYGVeDYkBxXBva.", Sha2Crypt.sha256Crypt("secret".getBytes(Charsets.UTF_8), "$5$rounds=50$abcd$", random));
66+
assertEquals("$5$rounds=1001$abcd$SQsJZs7KXKdd2DtklI3TY3tkD7UYA99RD0FBLm4Sk48", Sha2Crypt.sha256Crypt("secret".getBytes(Charsets.UTF_8), "$5$rounds=1001$abcd$", random));
67+
assertEquals("$5$rounds=9999$abcd$Rh/8ngVh9oyuS6lL3.fsq.9xbvXJsfyKWxSjO2mPIa7", Sha2Crypt.sha256Crypt("secret".getBytes(Charsets.UTF_8), "$5$rounds=9999$abcd", random));
68+
}
69+
6070
@Test
6171
public void testSha256CryptExplicitCall() {
6272
assertTrue(Sha2Crypt.sha256Crypt("secret".getBytes()).matches("^\\$5\\$[a-zA-Z0-9./]{0,16}\\$.{1,}$"));

src/test/java/org/apache/commons/codec/digest/Sha512CryptTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.junit.Assert.assertTrue;
2121

2222
import java.util.Arrays;
23+
import java.util.concurrent.ThreadLocalRandom;
2324

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

60+
@Test
61+
public void testSha512CryptExplicitCallThreadLocalRandom() {
62+
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
63+
assertTrue(Sha2Crypt.sha512Crypt("secret".getBytes(), null, threadLocalRandom).matches("^\\$6\\$[a-zA-Z0-9./]{0,16}\\$.{1,}$"));
64+
}
65+
5966
@Test(expected = NullPointerException.class)
6067
public void testSha512CryptNullData() {
6168
Sha2Crypt.sha512Crypt((byte[]) null);

0 commit comments

Comments
 (0)