blob: a546c5d52476d1b5efb4f7ff3d00ec729693e9f0 [file] [log] [blame]
package org.bouncycastle.crypto.generators;
import java.io.ByteArrayOutputStream;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Strings;
/**
* Password hashing scheme BCrypt,
* designed by Niels Provos and David Mazières, using the
* String format and the Base64 encoding
* of the reference implementation on OpenBSD
*/
public class OpenBSDBCrypt
{
private static final byte[] encodingTable = // the Bcrypts encoding table for OpenBSD
{
(byte)'.', (byte)'/', (byte)'A', (byte)'B', (byte)'C', (byte)'D',
(byte)'E', (byte)'F', (byte)'G', (byte)'H', (byte)'I', (byte)'J',
(byte)'K', (byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P',
(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', (byte)'V',
(byte)'W', (byte)'X', (byte)'Y', (byte)'Z', (byte)'a', (byte)'b',
(byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', (byte)'h',
(byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
(byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t',
(byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
(byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
(byte)'6', (byte)'7', (byte)'8', (byte)'9'
};
/*
* set up the decoding table.
*/
private static final byte[] decodingTable = new byte[128];
private static final String version = "2a"; // previous version was not UTF-8
static
{
for (int i = 0; i < decodingTable.length; i++)
{
decodingTable[i] = (byte)0xff;
}
for (int i = 0; i < encodingTable.length; i++)
{
decodingTable[encodingTable[i]] = (byte)i;
}
}
public OpenBSDBCrypt()
{
}
/**
* Creates a 60 character Bcrypt String, including
* version, cost factor, salt and hash, separated by '$'
*
* @param cost the cost factor, treated as an exponent of 2
* @param salt a 16 byte salt
* @param password the password
* @return a 60 character Bcrypt String
*/
private static String createBcryptString(
byte[] password,
byte[] salt,
int cost)
{
StringBuffer sb = new StringBuffer(60);
sb.append('$');
sb.append(version);
sb.append('$');
sb.append(cost < 10 ? ("0" + cost) : Integer.toString(cost));
sb.append('$');
sb.append(encodeData(salt));
byte[] key = BCrypt.generate(password, salt, cost);
sb.append(encodeData(key));
return sb.toString();
}
/**
* Creates a 60 character Bcrypt String, including
* version, cost factor, salt and hash, separated by '$'
*
* @param cost the cost factor, treated as an exponent of 2
* @param salt a 16 byte salt
* @param password the password
* @return a 60 character Bcrypt String
*/
public static String generate(
char[] password,
byte[] salt,
int cost)
{
if (password == null)
{
throw new IllegalArgumentException("Password required.");
}
if (salt == null)
{
throw new IllegalArgumentException("Salt required.");
}
else if (salt.length != 16)
{
throw new DataLengthException("16 byte salt required: " + salt.length);
}
if (cost < 4 || cost > 31) // Minimum rounds: 16, maximum 2^31
{
throw new IllegalArgumentException("Invalid cost factor.");
}
byte[] psw = Strings.toUTF8ByteArray(password);
// 0 termination:
byte[] tmp = new byte[psw.length >= 72 ? 72 : psw.length + 1];
if (tmp.length > psw.length)
{
System.arraycopy(psw, 0, tmp, 0, psw.length);
}
else
{
System.arraycopy(psw, 0, tmp, 0, tmp.length);
}
Arrays.fill(psw, (byte)0);
String rv = createBcryptString(tmp, salt, cost);
Arrays.fill(tmp, (byte)0);
return rv;
}
/**
* Checks if a password corresponds to a 60 character Bcrypt String
*
* @param bcryptString a 60 character Bcrypt String, including
* version, cost factor, salt and hash,
* separated by '$'
* @param password the password as an array of chars
* @return true if the password corresponds to the
* Bcrypt String, otherwise false
*/
public static boolean checkPassword(
String bcryptString,
char[] password)
{
// validate bcryptString:
if (bcryptString.length() != 60)
{
throw new DataLengthException("Bcrypt String length: "
+ bcryptString.length() + ", 60 required.");
}
if (bcryptString.charAt(0) != '$'
|| bcryptString.charAt(3) != '$'
|| bcryptString.charAt(6) != '$')
{
throw new IllegalArgumentException("Invalid Bcrypt String format.");
}
if (!bcryptString.substring(1, 3).equals(version))
{
throw new IllegalArgumentException("Wrong Bcrypt version, 2a expected.");
}
int cost = 0;
try
{
cost = Integer.parseInt(bcryptString.substring(4, 6));
}
catch (NumberFormatException nfe)
{
throw new IllegalArgumentException("Invalid cost factor: "
+ bcryptString.substring(4, 6));
}
if (cost < 4 || cost > 31)
{
throw new IllegalArgumentException("Invalid cost factor: "
+ cost + ", 4 < cost < 31 expected.");
}
// check password:
if (password == null)
{
throw new IllegalArgumentException("Missing password.");
}
byte[] salt = decodeSaltString(
bcryptString.substring(bcryptString.lastIndexOf('$') + 1,
bcryptString.length() - 31));
String newBcryptString = generate(password, salt, cost);
return bcryptString.equals(newBcryptString);
}
/*
* encode the input data producing a Bcrypt base 64 String.
*
* @param a byte representation of the salt or the password
* @return the Bcrypt base64 String
*/
private static String encodeData(
byte[] data)
{
if (data.length != 24 && data.length != 16) // 192 bit key or 128 bit salt expected
{
throw new DataLengthException("Invalid length: " + data.length + ", 24 for key or 16 for salt expected");
}
boolean salt = false;
if (data.length == 16)//salt
{
salt = true;
byte[] tmp = new byte[18];// zero padding
System.arraycopy(data, 0, tmp, 0, data.length);
data = tmp;
}
else // key
{
data[data.length - 1] = (byte)0;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int len = data.length;
int a1, a2, a3;
int i;
for (i = 0; i < len; i += 3)
{
a1 = data[i] & 0xff;
a2 = data[i + 1] & 0xff;
a3 = data[i + 2] & 0xff;
out.write(encodingTable[(a1 >>> 2) & 0x3f]);
out.write(encodingTable[((a1 << 4) | (a2 >>> 4)) & 0x3f]);
out.write(encodingTable[((a2 << 2) | (a3 >>> 6)) & 0x3f]);
out.write(encodingTable[a3 & 0x3f]);
}
String result = Strings.fromByteArray(out.toByteArray());
if (salt == true)// truncate padding
{
return result.substring(0, 22);
}
else
{
return result.substring(0, result.length() - 1);
}
}
/*
* decodes the bcrypt base 64 encoded SaltString
*
* @param a 22 character Bcrypt base 64 encoded String
* @return the 16 byte salt
* @exception DataLengthException if the length
* of parameter is not 22
* @exception InvalidArgumentException if the parameter
* contains a value other than from Bcrypts base 64 encoding table
*/
private static byte[] decodeSaltString(
String saltString)
{
char[] saltChars = saltString.toCharArray();
ByteArrayOutputStream out = new ByteArrayOutputStream(16);
byte b1, b2, b3, b4;
if (saltChars.length != 22)// bcrypt salt must be 22 (16 bytes)
{
throw new DataLengthException("Invalid base64 salt length: " + saltChars.length + " , 22 required.");
}
// check String for invalid characters:
for (int i = 0; i < saltChars.length; i++)
{
int value = saltChars[i];
if (value > 122 || value < 46 || (value > 57 && value < 65))
{
throw new IllegalArgumentException("Salt string contains invalid character: " + value);
}
}
// Padding: add two '\u0000'
char[] tmp = new char[22 + 2];
System.arraycopy(saltChars, 0, tmp, 0, saltChars.length);
saltChars = tmp;
int len = saltChars.length;
for (int i = 0; i < len; i += 4)
{
b1 = decodingTable[saltChars[i]];
b2 = decodingTable[saltChars[i + 1]];
b3 = decodingTable[saltChars[i + 2]];
b4 = decodingTable[saltChars[i + 3]];
out.write((b1 << 2) | (b2 >> 4));
out.write((b2 << 4) | (b3 >> 2));
out.write((b3 << 6) | b4);
}
byte[] saltBytes = out.toByteArray();
// truncate:
byte[] tmpSalt = new byte[16];
System.arraycopy(saltBytes, 0, tmpSalt, 0, tmpSalt.length);
saltBytes = tmpSalt;
return saltBytes;
}
}