From 8d24ebc24ff184f0bab0ee2a92ade9fbc4823a02 Mon Sep 17 00:00:00 2001 From: Joseph Walton Date: Wed, 4 Jul 2012 21:43:15 +1000 Subject: [PATCH] SEC-1990: Clean up jBCrypt and include its tests. Merge in changes from jBCrypt. - Use a ByteArrayOutputStream to cache bytes. - Pass a StringBuilder into encode_base64. - Refactor string comparison into its own method. - General clean up. --- .../security/crypto/bcrypt/BCrypt.java | 212 ++++++----- .../security/crypto/bcrypt/BCryptTest.java | 344 ++++++++++++++++++ 2 files changed, 457 insertions(+), 99 deletions(-) create mode 100755 crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTest.java diff --git a/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java b/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java index 7257fa49da3..0fcd09f06b4 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java +++ b/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java @@ -11,10 +11,11 @@ // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - package org.springframework.security.crypto.bcrypt; +import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; + import java.security.SecureRandom; /** @@ -58,16 +59,14 @@ * 10, and the valid range is 4 to 31. * * @author Damien Miller - * @version 0.2 */ public class BCrypt { // BCrypt parameters + private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; private static final int BCRYPT_SALT_LEN = 16; - // Blowfish parameters private static final int BLOWFISH_NUM_ROUNDS = 16; - // Initial contents of key schedule private static final int P_orig[] = { 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, @@ -334,13 +333,11 @@ public class BCrypt { 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 }; - // bcrypt IV: "OrpheanBeholderScryDoubt" static private final int bf_crypt_ciphertext[] = { 0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274 }; - // Table for Base64 encoding static private final char base64_code[] = { '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', @@ -350,7 +347,6 @@ public class BCrypt { 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; - // Table for Base64 decoding static private final byte index_64[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, @@ -367,29 +363,29 @@ public class BCrypt { 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1 }; - // Expanded Blowfish key private int P[]; private int S[]; /** * Encode a byte array using bcrypt's slightly-modified base64 - * encoding scheme. Note that this is *not* compatible with + * encoding scheme. Note that this is not compatible with * the standard MIME-base64 encoding. * * @param d the byte array to encode * @param len the number of bytes to encode - * @return base64-encoded string + * @param rs the destination buffer for the base64-encoded string * @exception IllegalArgumentException if the length is invalid */ - private static String encode_base64(byte d[], int len) - throws IllegalArgumentException { + static void encode_base64(byte d[], int len, + StringBuilder rs) + throws IllegalArgumentException { int off = 0; - StringBuffer rs = new StringBuffer(); int c1, c2; - if (len <= 0 || len > d.length) - throw new IllegalArgumentException ("Invalid len"); + if (len <= 0 || len > d.length) { + throw new IllegalArgumentException("Invalid len"); + } while (off < len) { c1 = d[off++] & 0xff; @@ -412,19 +408,19 @@ private static String encode_base64(byte d[], int len) rs.append(base64_code[c1 & 0x3f]); rs.append(base64_code[c2 & 0x3f]); } - return rs.toString(); } /** * Look up the 3 bits base64-encoded by the specified character, - * range-checking againt conversion table + * range-checking against conversion table * @param x the base64-encoded value * @return the decoded value of x */ private static byte char64(char x) { - if ((int)x < 0 || (int)x > index_64.length) + if (x > index_64.length) { return -1; - return index_64[(int)x]; + } + return index_64[x]; } /** @@ -436,45 +432,46 @@ private static byte char64(char x) { * @return an array containing the decoded bytes * @throws IllegalArgumentException if maxolen is invalid */ - private static byte[] decode_base64(String s, int maxolen) - throws IllegalArgumentException { - StringBuffer rs = new StringBuffer(); + static byte[] decode_base64(String s, int maxolen) + throws IllegalArgumentException { + ByteArrayOutputStream out = new ByteArrayOutputStream(maxolen); int off = 0, slen = s.length(), olen = 0; - byte ret[]; byte c1, c2, c3, c4, o; - if (maxolen <= 0) - throw new IllegalArgumentException ("Invalid maxolen"); + if (maxolen <= 0) { + throw new IllegalArgumentException("Invalid maxolen"); + } while (off < slen - 1 && olen < maxolen) { c1 = char64(s.charAt(off++)); c2 = char64(s.charAt(off++)); - if (c1 == -1 || c2 == -1) + if (c1 == -1 || c2 == -1) { break; - o = (byte)(c1 << 2); + } + o = (byte) (c1 << 2); o |= (c2 & 0x30) >> 4; - rs.append((char)o); - if (++olen >= maxolen || off >= slen) + out.write(o); + if (++olen >= maxolen || off >= slen) { break; + } c3 = char64(s.charAt(off++)); - if (c3 == -1) + if (c3 == -1) { break; - o = (byte)((c2 & 0x0f) << 4); + } + o = (byte) ((c2 & 0x0f) << 4); o |= (c3 & 0x3c) >> 2; - rs.append((char)o); - if (++olen >= maxolen || off >= slen) + out.write(o); + if (++olen >= maxolen || off >= slen) { break; + } c4 = char64(s.charAt(off++)); - o = (byte)((c3 & 0x03) << 6); + o = (byte) ((c3 & 0x03) << 6); o |= c4; - rs.append((char)o); + out.write(o); ++olen; } - ret = new byte[olen]; - for (off = 0; off < olen; off++) - ret[off] = (byte)rs.charAt(off); - return ret; + return out.toByteArray(); } /** @@ -531,8 +528,8 @@ private static int streamtoword(byte data[], int offp[]) { * Initialise the Blowfish key schedule */ private void init_key() { - P = (int[])P_orig.clone(); - S = (int[])S_orig.clone(); + P = (int[]) P_orig.clone(); + S = (int[]) S_orig.clone(); } /** @@ -541,12 +538,13 @@ private void init_key() { */ private void key(byte key[]) { int i; - int koffp[] = { 0 }; - int lr[] = { 0, 0 }; + int koffp[] = {0}; + int lr[] = {0, 0}; int plen = P.length, slen = S.length; - for (i = 0; i < plen; i++) + for (i = 0; i < plen; i++) { P[i] = P[i] ^ streamtoword(key, koffp); + } for (i = 0; i < plen; i += 2) { encipher(lr, 0); @@ -570,12 +568,13 @@ private void key(byte key[]) { */ private void ekskey(byte data[], byte key[]) { int i; - int koffp[] = { 0 }, doffp[] = { 0 }; - int lr[] = { 0, 0 }; + int koffp[] = {0}, doffp[] = {0}; + int lr[] = {0, 0}; int plen = P.length, slen = S.length; - for (i = 0; i < plen; i++) + for (i = 0; i < plen; i++) { P[i] = P[i] ^ streamtoword(key, koffp); + } for (i = 0; i < plen; i += 2) { lr[0] ^= streamtoword(data, doffp); @@ -594,6 +593,13 @@ private void ekskey(byte data[], byte key[]) { } } + static long roundsForLogRounds(int log_rounds) { + if (log_rounds < 4 || log_rounds > 31) { + throw new IllegalArgumentException("Bad number of rounds"); + } + return 1L << log_rounds; + } + /** * Perform the central password hashing step in the * bcrypt scheme @@ -604,35 +610,31 @@ private void ekskey(byte data[], byte key[]) { * @return an array containing the binary hashed password */ private byte[] crypt_raw(byte password[], byte salt[], int log_rounds) { - int rounds, i, j; - int cdata[] = (int[])bf_crypt_ciphertext.clone(); + int cdata[] = (int[]) bf_crypt_ciphertext.clone(); int clen = cdata.length; byte ret[]; - if (log_rounds < 4 || log_rounds > 31) - throw new IllegalArgumentException ("Bad number of rounds"); - rounds = 1 << log_rounds; - if (salt.length != BCRYPT_SALT_LEN) - throw new IllegalArgumentException ("Bad salt length"); + long rounds = roundsForLogRounds(log_rounds); init_key(); ekskey(salt, password); - for (i = 0; i != rounds; i++) { + for (long i = 0; i < rounds; i++) { key(password); key(salt); } - for (i = 0; i < 64; i++) { - for (j = 0; j < (clen >> 1); j++) + for (int i = 0; i < 64; i++) { + for (int j = 0; j < (clen >> 1); j++) { encipher(cdata, j << 1); + } } ret = new byte[clen * 4]; - for (i = 0, j = 0; i < clen; i++) { - ret[j++] = (byte)((cdata[i] >> 24) & 0xff); - ret[j++] = (byte)((cdata[i] >> 16) & 0xff); - ret[j++] = (byte)((cdata[i] >> 8) & 0xff); - ret[j++] = (byte)(cdata[i] & 0xff); + for (int i = 0, j = 0; i < clen; i++) { + ret[j++] = (byte) ((cdata[i] >> 24) & 0xff); + ret[j++] = (byte) ((cdata[i] >> 16) & 0xff); + ret[j++] = (byte) ((cdata[i] >> 8) & 0xff); + ret[j++] = (byte) (cdata[i] & 0xff); } return ret; } @@ -648,24 +650,37 @@ public static String hashpw(String password, String salt) { BCrypt B; String real_salt; byte passwordb[], saltb[], hashed[]; - char minor = (char)0; + char minor = (char) 0; int rounds, off = 0; - StringBuffer rs = new StringBuffer(); + StringBuilder rs = new StringBuilder(); - if (salt.charAt(0) != '$' || salt.charAt(1) != '2') - throw new IllegalArgumentException ("Invalid salt version"); - if (salt.charAt(2) == '$') + int saltLength = salt.length(); + + if (saltLength < 28) { + throw new IllegalArgumentException("Invalid salt"); + } + + if (salt.charAt(0) != '$' || salt.charAt(1) != '2') { + throw new IllegalArgumentException("Invalid salt version"); + } + if (salt.charAt(2) == '$') { off = 3; - else { + } else { minor = salt.charAt(2); - if (minor != 'a' || salt.charAt(3) != '$') - throw new IllegalArgumentException ("Invalid salt revision"); + if (minor != 'a' || salt.charAt(3) != '$') { + throw new IllegalArgumentException("Invalid salt revision"); + } off = 4; } + if (saltLength - off < 25) { + throw new IllegalArgumentException("Invalid salt"); + } + // Extract number of rounds - if (salt.charAt(off + 2) > '$') - throw new IllegalArgumentException ("Missing salt rounds"); + if (salt.charAt(off + 2) > '$') { + throw new IllegalArgumentException("Missing salt rounds"); + } rounds = Integer.parseInt(salt.substring(off, off + 2)); real_salt = salt.substring(off + 3, off + 25); @@ -681,20 +696,18 @@ public static String hashpw(String password, String salt) { hashed = B.crypt_raw(passwordb, saltb, rounds); rs.append("$2"); - if (minor >= 'a') + if (minor >= 'a') { rs.append(minor); + } rs.append("$"); - if (rounds < 10) + if (rounds < 10) { rs.append("0"); - if (rounds > 31) { - throw new IllegalArgumentException( - "rounds exceeds maximum (31)"); } - rs.append(Integer.toString(rounds)); + rs.append(rounds); rs.append("$"); - rs.append(encode_base64(saltb, saltb.length)); - rs.append(encode_base64(hashed, - bf_crypt_ciphertext.length * 4 - 1)); + encode_base64(saltb, saltb.length, rs); + encode_base64(hashed, + bf_crypt_ciphertext.length * 4 - 1, rs); return rs.toString(); } @@ -707,21 +720,21 @@ public static String hashpw(String password, String salt) { * @return an encoded salt value */ public static String gensalt(int log_rounds, SecureRandom random) { - StringBuffer rs = new StringBuffer(); + if (log_rounds < 4 || log_rounds > 31) { + throw new IllegalArgumentException("Bad number of rounds"); + } + StringBuilder rs = new StringBuilder(); byte rnd[] = new byte[BCRYPT_SALT_LEN]; random.nextBytes(rnd); rs.append("$2a$"); - if (log_rounds < 10) + if (log_rounds < 10) { rs.append("0"); - if (log_rounds > 31) { - throw new IllegalArgumentException( - "log_rounds exceeds maximum (31)"); } - rs.append(Integer.toString(log_rounds)); + rs.append(log_rounds); rs.append("$"); - rs.append(encode_base64(rnd, rnd.length)); + encode_base64(rnd, rnd.length, rs); return rs.toString(); } @@ -754,20 +767,21 @@ public static String gensalt() { * @return true if the passwords match, false otherwise */ public static boolean checkpw(String plaintext, String hashed) { - byte hashed_bytes[]; - byte try_bytes[]; - try { - String try_pw = hashpw(plaintext, hashed); - hashed_bytes = hashed.getBytes("UTF-8"); - try_bytes = try_pw.getBytes("UTF-8"); - } catch (UnsupportedEncodingException uee) { + return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed)); + } + + static boolean equalsNoEarlyReturn(String a, String b) { + char[] caa = a.toCharArray(); + char[] cab = b.toCharArray(); + + if (caa.length != cab.length) { return false; } - if (hashed_bytes.length != try_bytes.length) - return false; + byte ret = 0; - for (int i = 0; i < try_bytes.length; i++) - ret |= hashed_bytes[i] ^ try_bytes[i]; + for (int i = 0; i < caa.length; i++) { + ret |= caa[i] ^ cab[i]; + } return ret == 0; } } diff --git a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTest.java b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTest.java new file mode 100755 index 00000000000..d5d9fcf607f --- /dev/null +++ b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTest.java @@ -0,0 +1,344 @@ +// Copyright (c) 2006 Damien Miller +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package org.springframework.security.crypto.bcrypt; + +import java.util.Arrays; + +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * JUnit unit tests for BCrypt routines + * @author Damien Miller + */ +public class BCryptTest { + + private static void print(String s) + { +// System.out.print(s); + } + + private static void println(String s) + { +// System.out.println(s); + } + + String test_vectors[][] = { + {"", + "$2a$06$DCq7YPn5Rq63x1Lad4cll.", + "$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s."}, + {"", + "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.", + "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.Tl.ZHfXLhvt/SgVyWhQqgqcZ7ZuUtye"}, + {"", + "$2a$10$k1wbIrmNyFAPwPVPSVa/ze", + "$2a$10$k1wbIrmNyFAPwPVPSVa/zecw2BCEnBwVS2GbrmgzxFUOqW9dk4TCW"}, + {"", + "$2a$12$k42ZFHFWqBp3vWli.nIn8u", + "$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO"}, + {"a", + "$2a$06$m0CrhHm10qJ3lXRY.5zDGO", + "$2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe"}, + {"a", + "$2a$08$cfcvVd2aQ8CMvoMpP2EBfe", + "$2a$08$cfcvVd2aQ8CMvoMpP2EBfeodLEkkFJ9umNEfPD18.hUF62qqlC/V."}, + {"a", + "$2a$10$k87L/MF28Q673VKh8/cPi.", + "$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u"}, + {"a", + "$2a$12$8NJH3LsPrANStV6XtBakCe", + "$2a$12$8NJH3LsPrANStV6XtBakCez0cKHXVxmvxIlcz785vxAIZrihHZpeS"}, + {"abc", + "$2a$06$If6bvum7DFjUnE9p2uDeDu", + "$2a$06$If6bvum7DFjUnE9p2uDeDu0YHzrHM6tf.iqN8.yx.jNN1ILEf7h0i"}, + {"abc", + "$2a$08$Ro0CUfOqk6cXEKf3dyaM7O", + "$2a$08$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm"}, + {"abc", + "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.", + "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi"}, + {"abc", + "$2a$12$EXRkfkdmXn2gzds2SSitu.", + "$2a$12$EXRkfkdmXn2gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q"}, + {"abcdefghijklmnopqrstuvwxyz", + "$2a$06$.rCVZVOThsIa97pEDOxvGu", + "$2a$06$.rCVZVOThsIa97pEDOxvGuRRgzG64bvtJ0938xuqzv18d3ZpQhstC"}, + {"abcdefghijklmnopqrstuvwxyz", + "$2a$08$aTsUwsyowQuzRrDqFflhge", + "$2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz."}, + {"abcdefghijklmnopqrstuvwxyz", + "$2a$10$fVH8e28OQRj9tqiDXs1e1u", + "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq"}, + {"abcdefghijklmnopqrstuvwxyz", + "$2a$12$D4G5f18o7aMMfwasBL7Gpu", + "$2a$12$D4G5f18o7aMMfwasBL7GpuQWuP3pkrZrOAnqP.bmezbMng.QwJ/pG"}, + {"~!@#$%^&*() ~!@#$%^&*()PNBFRD", + "$2a$06$fPIsBO8qRqkjj273rfaOI.", + "$2a$06$fPIsBO8qRqkjj273rfaOI.HtSV9jLDpTbZn782DC6/t7qT67P6FfO"}, + {"~!@#$%^&*() ~!@#$%^&*()PNBFRD", + "$2a$08$Eq2r4G/76Wv39MzSX262hu", + "$2a$08$Eq2r4G/76Wv39MzSX262huzPz612MZiYHVUJe/OcOql2jo4.9UxTW"}, + {"~!@#$%^&*() ~!@#$%^&*()PNBFRD", + "$2a$10$LgfYWkbzEvQ4JakH7rOvHe", + "$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS"}, + {"~!@#$%^&*() ~!@#$%^&*()PNBFRD", + "$2a$12$WApznUOJfkEGSmYRfnkrPO", + "$2a$12$WApznUOJfkEGSmYRfnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC"} + }; + + /** + * Test method for 'BCrypt.hashpw(String, String)' + */ + @Test + public void testHashpw() { + print("BCrypt.hashpw(): "); + for (int i = 0; i < test_vectors.length; i++) { + String plain = test_vectors[i][0]; + String salt = test_vectors[i][1]; + String expected = test_vectors[i][2]; + String hashed = BCrypt.hashpw(plain, salt); + assertEquals(hashed, expected); + print("."); + } + println(""); + } + + /** + * Test method for 'BCrypt.gensalt(int)' + */ + @Test + public void testGensaltInt() { + print("BCrypt.gensalt(log_rounds):"); + for (int i = 4; i <= 12; i++) { + print(" " + Integer.toString(i) + ":"); + for (int j = 0; j < test_vectors.length; j += 4) { + String plain = test_vectors[j][0]; + String salt = BCrypt.gensalt(i); + String hashed1 = BCrypt.hashpw(plain, salt); + String hashed2 = BCrypt.hashpw(plain, hashed1); + assertEquals(hashed1, hashed2); + print("."); + } + } + println(""); + } + + /** + * Test method for 'BCrypt.gensalt()' + */ + @Test + public void testGensalt() { + print("BCrypt.gensalt(): "); + for (int i = 0; i < test_vectors.length; i += 4) { + String plain = test_vectors[i][0]; + String salt = BCrypt.gensalt(); + String hashed1 = BCrypt.hashpw(plain, salt); + String hashed2 = BCrypt.hashpw(plain, hashed1); + assertEquals(hashed1, hashed2); + print("."); + } + println(""); + } + + /** + * Test method for 'BCrypt.checkpw(String, String)' + * expecting success + */ + @Test + public void testCheckpw_success() { + print("BCrypt.checkpw w/ good passwords: "); + for (int i = 0; i < test_vectors.length; i++) { + String plain = test_vectors[i][0]; + String expected = test_vectors[i][2]; + assertTrue(BCrypt.checkpw(plain, expected)); + print("."); + } + println(""); + } + + /** + * Test method for 'BCrypt.checkpw(String, String)' + * expecting failure + */ + @Test + public void testCheckpw_failure() { + print("BCrypt.checkpw w/ bad passwords: "); + for (int i = 0; i < test_vectors.length; i++) { + int broken_index = (i + 4) % test_vectors.length; + String plain = test_vectors[i][0]; + String expected = test_vectors[broken_index][2]; + assertFalse(BCrypt.checkpw(plain, expected)); + print("."); + } + println(""); + } + + /** + * Test for correct hashing of non-US-ASCII passwords + */ + @Test + public void testInternationalChars() { + print("BCrypt.hashpw w/ international chars: "); + String pw1 = "ππππππππ"; + String pw2 = "????????"; + + String h1 = BCrypt.hashpw(pw1, BCrypt.gensalt()); + assertFalse(BCrypt.checkpw(pw2, h1)); + print("."); + + String h2 = BCrypt.hashpw(pw2, BCrypt.gensalt()); + assertFalse(BCrypt.checkpw(pw1, h2)); + print("."); + println(""); + } + + @Test + public void roundsForDoesNotOverflow() + { + assertEquals(1024, BCrypt.roundsForLogRounds(10)); + assertEquals(0x80000000L, BCrypt.roundsForLogRounds(31)); + } + + @Test(expected = IllegalArgumentException.class) + public void emptyByteArrayCannotBeEncoded() + { + BCrypt.encode_base64(new byte[0], 0, new StringBuilder()); + } + + @Test(expected = IllegalArgumentException.class) + public void moreBytesThanInTheArrayCannotBeEncoded() + { + BCrypt.encode_base64(new byte[1], 2, new StringBuilder()); + } + + @Test(expected = IllegalArgumentException.class) + public void decodingMustRequestMoreThanZeroBytes() + { + BCrypt.decode_base64("", 0); + } + + private static String encode_base64(byte d[], int len) + throws IllegalArgumentException + { + StringBuilder rs = new StringBuilder(); + BCrypt.encode_base64(d, len, rs); + return rs.toString(); + } + + @Test + public void testBase64EncodeSimpleByteArrays() + { + assertEquals("..", encode_base64(new byte[]{0}, 1)); + assertEquals("...", encode_base64(new byte[]{0, 0}, 2)); + assertEquals("....", encode_base64(new byte[]{0, 0, 0}, 3)); + } + + @Test + public void decodingCharsOutsideAsciiGivesNoResults() + { + byte[] ba = BCrypt.decode_base64("ππππππππ", 1); + assertEquals(0, ba.length); + } + + @Test + public void decodingStopsWithFirstInvalidCharacter() + { + assertEquals(1, BCrypt.decode_base64("....", 1).length); + assertEquals(0, BCrypt.decode_base64(" ....", 1).length); + } + + @Test + public void decodingOnlyProvidesAvailableBytes() + { + assertEquals(0, BCrypt.decode_base64("", 1).length); + assertEquals(3, BCrypt.decode_base64("......", 3).length); + assertEquals(4, BCrypt.decode_base64("......", 4).length); + assertEquals(4, BCrypt.decode_base64("......", 5).length); + } + + /** + * Encode and decode each byte value in each position. + */ + @Test + public void testBase64EncodeDecode() + { + byte[] ba = new byte[3]; + + for (int b = 0; b <= 0xFF; b++) { + for (int i = 0; i < ba.length; i++) { + Arrays.fill(ba, (byte) 0); + ba[i] = (byte) b; + + String s = encode_base64(ba, 3); + assertEquals(4, s.length()); + + byte[] decoded = BCrypt.decode_base64(s, 3); + assertArrayEquals(ba, decoded); + } + } + } + + @Test(expected = IllegalArgumentException.class) + public void genSaltFailsWithTooFewRounds() { + BCrypt.gensalt(3); + } + + @Test(expected = IllegalArgumentException.class) + public void genSaltFailsWithTooManyRounds() { + BCrypt.gensalt(32); + } + + @Test + public void genSaltGeneratesCorrectSaltPrefix() { + assertTrue(BCrypt.gensalt(4).startsWith("$2a$04$")); + assertTrue(BCrypt.gensalt(31).startsWith("$2a$31$")); + } + + @Test(expected = IllegalArgumentException.class) + public void hashpwFailsWhenSaltSpecifiesTooFewRounds() { + BCrypt.hashpw("password", "$2a$03$......................"); + } + + @Test(expected = IllegalArgumentException.class) + public void hashpwFailsWhenSaltSpecifiesTooManyRounds() { + BCrypt.hashpw("password", "$2a$32$......................"); + } + + @Test(expected = IllegalArgumentException.class) + public void saltLengthIsChecked() + { + BCrypt.hashpw("", ""); + } + + @Test + public void hashpwWorksWithOldRevision() + { + assertEquals( + "$2$05$......................bvpG2UfzdyW/S0ny/4YyEZrmczoJfVm", + BCrypt.hashpw("password", "$2$05$......................")); + } + + @Test + public void equalsOnStringsIsCorrect() + { + assertTrue(BCrypt.equalsNoEarlyReturn("", "")); + assertTrue(BCrypt.equalsNoEarlyReturn("test", "test")); + + assertFalse(BCrypt.equalsNoEarlyReturn("test", "")); + assertFalse(BCrypt.equalsNoEarlyReturn("", "test")); + + assertFalse(BCrypt.equalsNoEarlyReturn("test", "pass")); + } +}