浏览代码

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.
Joseph Walton 13 年之前
父节点
当前提交
14a5135ac3

+ 113 - 99
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 @@ import java.security.SecureRandom;
  * 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 <strong>not</strong> 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 @@ public class BCrypt {
             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 @@ public class BCrypt {
      * @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 @@ public class BCrypt {
      * 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 @@ public class BCrypt {
      */
     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 @@ public class BCrypt {
      */
     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 @@ public class BCrypt {
         }
     }
 
+    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 @@ public class BCrypt {
      * @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 class BCrypt {
         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 class BCrypt {
         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 class BCrypt {
      * @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 class BCrypt {
      * @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;
     }
 }

+ 344 - 0
crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTest.java

@@ -0,0 +1,344 @@
+// Copyright (c) 2006 Damien Miller <djm@mindrot.org>
+//
+// 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"));
+    }
+}