Update and Fix PHP bcrypt to login server bcrypt compatibility (#462)

Update jBCrypt from version 0.2 to 0.4 which resolves a security bug with character encoding and corrects an integer overflow bug.
Modify jBCrypt library to handle $2y$ hashes, which are produced by PHP's password_hash bcrypt function.  The original jBCrypt library only recognizes $2a$ hashes which makes it harder for a website CMS (content management system) to interface with the server's login and databases.

Credit to: Ariel Salomon for compatibility support of 2b, 2y, and 2x hashes fe14360010 (diff-61d1d5b7b3c721fac60702cf559d70aa)
This commit is contained in:
jongames
2019-05-04 10:36:12 -04:00
committed by Ronan Lana
parent 618d53c707
commit e70215eb9d

View File

@@ -11,12 +11,14 @@
// 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 tools;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.Arrays;
/**
* BCrypt implements OpenBSD-style Blowfish password hashing using
* the scheme described in "A Future-Adaptable Password Scheme" by
@@ -58,16 +60,15 @@ import java.security.SecureRandom;
* 10, and the valid range is 4 to 30.
*
* @author Damien Miller
* @version 0.2
* @version 0.4
*/
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,7 +335,6 @@ public class BCrypt {
0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6
};
// bcrypt IV: "OrpheanBeholderScryDoubt". The C implementation calls
// this "ciphertext", but it is really plaintext or an IV. We keep
// the name to make code comparison easier.
@@ -342,7 +342,6 @@ public class BCrypt {
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',
@@ -352,7 +351,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,
@@ -369,7 +367,6 @@ 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[];
@@ -387,11 +384,12 @@ public class BCrypt {
private static String encode_base64(byte d[], int len)
throws IllegalArgumentException {
int off = 0;
StringBuffer rs = new StringBuffer();
StringBuilder rs = new StringBuilder();
int c1, c2;
if (len <= 0 || len > d.length)
if (len <= 0 || len > d.length) {
throw new IllegalArgumentException("Invalid len");
}
while (off < len) {
c1 = d[off++] & 0xff;
@@ -424,8 +422,9 @@ public class BCrypt {
* @return the decoded value of x
*/
private static byte char64(char x) {
if ((int)x < 0 || (int)x > index_64.length)
if ((int) x < 0 || (int) x > index_64.length) {
return -1;
}
return index_64[(int) x];
}
@@ -440,32 +439,37 @@ public class BCrypt {
*/
private static byte[] decode_base64(String s, int maxolen)
throws IllegalArgumentException {
StringBuffer rs = new StringBuffer();
StringBuilder rs = new StringBuilder();
int off = 0, slen = s.length(), olen = 0;
byte ret[];
byte c1, c2, c3, c4, o;
if (maxolen <= 0)
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 |= (c2 & 0x30) >> 4;
rs.append((char) o);
if (++olen >= maxolen || off >= slen)
if (++olen >= maxolen || off >= slen) {
break;
}
c3 = char64(s.charAt(off++));
if (c3 == -1)
if (c3 == -1) {
break;
}
o = (byte) ((c2 & 0x0f) << 4);
o |= (c3 & 0x3c) >> 2;
rs.append((char) o);
if (++olen >= maxolen || off >= slen)
if (++olen >= maxolen || off >= slen) {
break;
}
c4 = char64(s.charAt(off++));
o = (byte) ((c3 & 0x03) << 6);
o |= c4;
@@ -474,8 +478,9 @@ public class BCrypt {
}
ret = new byte[olen];
for (off = 0; off < olen; off++)
for (off = 0; off < olen; off++) {
ret[off] = (byte) rs.charAt(off);
}
return ret;
}
@@ -513,20 +518,50 @@ public class BCrypt {
* @param data the string to extract the data from
* @param offp a "pointer" (as a one-entry array) to the
* current offset into data
* @return the next word of material from data
* @param signp a "pointer" (as a one-entry array) to the
* cumulative flag for non-benign sign extension
* @return correct and buggy next word of material from data as int[2]
*/
private static int streamtoword(byte data[], int offp[]) {
private static int[] streamtowords(byte data[], int offp[], int signp[]) {
int i;
int word = 0;
int words[] = { 0, 0 };
int off = offp[0];
int sign = signp[0];
for (i = 0; i < 4; i++) {
word = (word << 8) | (data[off] & 0xff);
words[0] = (words[0] << 8) | (data[off] & 0xff);
words[1] = (words[1] << 8) | (int)data[off];
if (i > 0) sign |= words[1] & 0x80;
off = (off + 1) % data.length;
}
offp[0] = off;
return word;
signp[0] = sign;
return words;
}
/**
* Cycically extract a word of key material
* @param data the string to extract the data from
* @param offp a "pointer" (as a one-entry array) to the
* current offset into data
* @return the next word of material from data
*/
private static int streamtoword(byte data[], int offp[]) {
int signp[] = { 0 };
return streamtowords(data, offp, signp)[0];
}
/**
* Cycically extract a word of key material, with sign-extension bug
* @param data the string to extract the data from
* @param offp a "pointer" (as a one-entry array) to the
* current offset into data
* @return the next word of material from data
*/
private static int streamtoword_bug(byte data[], int offp[]) {
int signp[] = { 0 };
return streamtowords(data, offp, signp)[1];
}
/**
@@ -540,15 +575,20 @@ public class BCrypt {
/**
* Key the Blowfish cipher
* @param key an array containing the key
* @param sign_ext_bug true to implement the 2x bug
*/
private void key(byte key[]) {
private void key(byte key[], boolean sign_ext_bug) {
int i;
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++) {
if (!sign_ext_bug)
P[i] = P[i] ^ streamtoword(key, koffp);
else
P[i] = P[i] ^ streamtoword_bug(key, koffp);
}
for (i = 0; i < plen; i += 2) {
encipher(lr, 0);
@@ -569,15 +609,53 @@ public class BCrypt {
* http://www.openbsd.org/papers/bcrypt-paper.ps
* @param data salt information
* @param key password information
* @param sign_ext_bug true to implement the 2x bug
* @param safety bit 16 is set when the safety measure is requested
*/
private void ekskey(byte data[], byte key[]) {
private void ekskey(byte data[], byte key[],
boolean sign_ext_bug, int safety) {
int i;
int koffp[] = {0}, doffp[] = {0};
int lr[] = {0, 0};
int plen = P.length, slen = S.length;
int signp[] = { 0 }; // non-benign sign-extension flag
int diff = 0; // zero iff correct and buggy are same
for (i = 0; i < plen; i++)
P[i] = P[i] ^ streamtoword(key, koffp);
for (i = 0; i < plen; i++) {
int words[] = streamtowords(key, koffp, signp);
diff |= words[0] ^ words[1];
P[i] = P[i] ^ words[sign_ext_bug ? 1 : 0];
}
int sign = signp[0];
/*
* At this point, "diff" is zero iff the correct and buggy algorithms produced
* exactly the same result. If so and if "sign" is non-zero, which indicates
* that there was a non-benign sign extension, this means that we have a
* collision between the correctly computed hash for this password and a set of
* passwords that could be supplied to the buggy algorithm. Our safety measure
* is meant to protect from such many-buggy to one-correct collisions, by
* deviating from the correct algorithm in such cases. Let's check for this.
*/
diff |= diff >> 16; /* still zero iff exact match */
diff &= 0xffff; /* ditto */
diff += 0xffff; /* bit 16 set iff "diff" was non-zero (on non-match) */
sign <<= 9; /* move the non-benign sign extension flag to bit 16 */
sign &= ~diff & safety; /* action needed? */
/*
* If we have determined that we need to deviate from the correct algorithm,
* flip bit 16 in initial expanded key. (The choice of 16 is arbitrary, but
* let's stick to it now. It came out of the approach we used above, and it's
* not any worse than any other choice we could make.)
*
* It is crucial that we don't do the same to the expanded key used in the main
* Eksblowfish loop. By doing it to only one of these two, we deviate from a
* state that could be directly specified by a password to the buggy algorithm
* (and to the fully correct one as well, but that's a side-effect).
*/
P[0] ^= sign;
for (i = 0; i < plen; i += 2) {
lr[0] ^= streamtoword(data, doffp);
@@ -603,32 +681,37 @@ public class BCrypt {
* @param salt the binary salt to hash with the password
* @param log_rounds the binary logarithm of the number
* of rounds of hashing to apply
* @param sign_ext_bug true to implement the 2x bug
* @param safety bit 16 is set when the safety measure is requested
* @param cdata the plaintext to encrypt
* @return an array containing the binary hashed password
*/
public byte[] crypt_raw(byte password[], byte salt[], int log_rounds,
int cdata[]) {
private byte[] crypt_raw(byte password[], byte salt[], int log_rounds,
boolean sign_ext_bug, int safety, int cdata[]) {
int rounds, i, j;
int clen = cdata.length;
byte ret[];
if (log_rounds < 4 || log_rounds > 30)
if (log_rounds < 4 || log_rounds > 30) {
throw new IllegalArgumentException("Bad number of rounds");
}
rounds = 1 << log_rounds;
if (salt.length != BCRYPT_SALT_LEN)
if (salt.length != BCRYPT_SALT_LEN) {
throw new IllegalArgumentException("Bad salt length");
}
init_key();
ekskey(salt, password);
ekskey(salt, password, sign_ext_bug, safety);
for (i = 0; i != rounds; i++) {
key(password);
key(salt);
key(password, sign_ext_bug);
key(salt, false);
}
for (i = 0; i < 64; i++) {
for (j = 0; j < (clen >> 1); j++)
for (j = 0; j < (clen >> 1); j++) {
encipher(cdata, j << 1);
}
}
ret = new byte[clen * 4];
for (i = 0, j = 0; i < clen; i++) {
@@ -640,6 +723,23 @@ public class BCrypt {
return ret;
}
/**
* Converts given plaintext to byte representation.
* @param plaintext the plaintext password to convert
* @return Byte representation of given plaintext.
*/
private static byte[] stringToBytes(String plaintext) {
byte plaintextb[];
try {
plaintextb = plaintext.getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
throw new AssertionError("UTF-8 is not supported");
}
return plaintextb;
}
/**
* Hash a password using the OpenBSD bcrypt scheme
* @param password the password to hash
@@ -648,48 +748,67 @@ public class BCrypt {
* @return the hashed password
*/
public static String hashpw(String password, String salt) {
byte passwordb[] = stringToBytes(password);
return hashpw(passwordb, salt);
}
/**
* Hash a password using the OpenBSD bcrypt scheme
* @param passwordb the password to hash, as a byte array
* @param salt the salt to hash with (perhaps generated
* using BCrypt.gensalt)
* @return the hashed password
*/
public static String hashpw(byte passwordb[], String salt) {
BCrypt B;
String real_salt;
byte passwordb[], saltb[], hashed[];
byte saltb[], hashed[];
char minor = (char) 0;
int rounds, off = 0;
StringBuffer rs = new StringBuffer();
StringBuilder rs = new StringBuilder();
if (salt.charAt(0) != '$' || salt.charAt(1) != '2')
if (salt.charAt(0) != '$' || salt.charAt(1) != '2') {
throw new IllegalArgumentException("Invalid salt version");
if (salt.charAt(2) == '$')
}
if (salt.charAt(2) == '$') {
off = 3;
else {
} else {
minor = salt.charAt(2);
if (minor != 'a' || salt.charAt(3) != '$')
if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b')
|| salt.charAt(3) != '$') {
throw new IllegalArgumentException("Invalid salt revision");
}
off = 4;
}
// Extract number of rounds
if (salt.charAt(off + 2) > '$')
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);
try {
passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
throw new AssertionError("UTF-8 is not supported");
}
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
if (minor >= 'a') // add null terminator
passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
B = new BCrypt();
hashed = B.crypt_raw(passwordb, saltb, rounds,
minor == 'x', // true for sign extension bug ('2x')
minor == 'a' ? 0x10000 : 0, // safety factor for '2a'
(int[])bf_crypt_ciphertext.clone());
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 > 30) {
throw new IllegalArgumentException(
"rounds exceeds maximum (30)");
@@ -704,21 +823,36 @@ public class BCrypt {
/**
* Generate a salt for use with the BCrypt.hashpw() method
* @param prefix the prefix value (default $2y)
* @param log_rounds the log2 of the number of rounds of
* hashing to apply - the work factor therefore increases as
* 2**log_rounds.
* @param random an instance of SecureRandom to use
* @return an encoded salt value
* @exception IllegalArgumentException if prefix or log_rounds is invalid
*/
public static String gensalt(int log_rounds, SecureRandom random) {
StringBuffer rs = new StringBuffer();
public static String gensalt(String prefix, int log_rounds, SecureRandom random)
throws IllegalArgumentException {
StringBuilder rs = new StringBuilder();
byte rnd[] = new byte[BCRYPT_SALT_LEN];
if (!prefix.startsWith("$2") ||
(prefix.charAt(2) != 'a' && prefix.charAt(2) != 'y') &&
prefix.charAt(2) != 'b') {
throw new IllegalArgumentException ("Invalid prefix");
}
if (log_rounds < 4 || log_rounds > 31) {
throw new IllegalArgumentException ("Invalid log_rounds");
}
random.nextBytes(rnd);
rs.append("$2a$");
if (log_rounds < 10)
rs.append("$2");
rs.append(prefix.charAt(2));
rs.append("$");
if (log_rounds < 10) {
rs.append("0");
}
if (log_rounds > 30) {
throw new IllegalArgumentException(
"log_rounds exceeds maximum (30)");
@@ -731,12 +865,42 @@ public class BCrypt {
/**
* Generate a salt for use with the BCrypt.hashpw() method
* @param prefix the prefix value (default $2y)
* @param log_rounds the log2 of the number of rounds of
* hashing to apply - the work factor therefore increases as
* 2**log_rounds.
* @return an encoded salt value
* @exception IllegalArgumentException if prefix or log_rounds is invalid
*/
public static String gensalt(int log_rounds) {
public static String gensalt(String prefix, int log_rounds)
throws IllegalArgumentException {
return gensalt(prefix, log_rounds, new SecureRandom());
}
/**
* Generate a salt for use with the BCrypt.hashpw() method
* @param log_rounds the log2 of the number of rounds of
* hashing to apply - the work factor therefore increases as
* 2**log_rounds.
* @param random an instance of SecureRandom to use
* @return an encoded salt value
* @exception IllegalArgumentException if prefix or log_rounds is invalid
*/
public static String gensalt(int log_rounds, SecureRandom random)
throws IllegalArgumentException {
return gensalt("$2y", log_rounds, random);
}
/**
* Generate a salt for use with the BCrypt.hashpw() method
* @param log_rounds the log2 of the number of rounds of
* hashing to apply - the work factor therefore increases as
* 2**log_rounds.
* @return an encoded salt value
* @exception IllegalArgumentException if prefix or log_rounds is invalid
*/
public static String gensalt(int log_rounds)
throws IllegalArgumentException {
return gensalt(log_rounds, new SecureRandom());
}
@@ -745,8 +909,10 @@ public class BCrypt {
* selecting a reasonable default for the number of hashing
* rounds to apply
* @return an encoded salt value
* @exception IllegalArgumentException if prefix or log_rounds is invalid
*/
public static String gensalt() {
public static String gensalt()
throws IllegalArgumentException {
return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);
}
@@ -758,6 +924,18 @@ public class BCrypt {
* @return true if the passwords match, false otherwise
*/
public static boolean checkpw(String plaintext, String hashed) {
byte plaintextb[] = stringToBytes(plaintext);
return checkpw(plaintextb, hashed);
}
/**
* Check that a plaintext byte[] password matches a previously hashed
* one
* @param plaintext the plaintext password to verify
* @param hashed the previously-hashed password
* @return true if the passwords match, false otherwise
*/
public static boolean checkpw(byte[] plaintext, String hashed) {
byte hashed_bytes[];
byte try_bytes[];
try {