org.cryptacular.bean.BCryptHashBean Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com.liferay.saml.opensaml.integration Show documentation
Show all versions of com.liferay.saml.opensaml.integration Show documentation
Liferay SAML OpenSAML Integration
/* See LICENSE for licensing and NOTICE for copyright. */
package org.cryptacular.bean;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import org.bouncycastle.crypto.generators.BCrypt;
import org.cryptacular.CryptoException;
import org.cryptacular.StreamException;
import org.cryptacular.codec.Base64Decoder;
import org.cryptacular.codec.Base64Encoder;
import org.cryptacular.codec.Decoder;
import org.cryptacular.codec.Encoder;
import org.cryptacular.util.ByteUtil;
/**
* {@link HashBean} implementation that uses the bcrypt algorithm for hashing. Hash strings of the following
* format are supported:
*
*
* $2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
*
* where:
* n is an optional bcrypt algorithm version (typically "a" or "b")
* 4 ≤ cost ≤ 31
* x is 22 characters of encoded salt
* y is 31 characters of encoded hash bytes
*
*
* The encoding for salt and hash bytes is a variant of base-64 encoding without padding in the following alphabet:
*
*
* ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
*
* @author Middleware Services
*/
public class BCryptHashBean implements HashBean
{
/** Custom base-64 alphabet. */
private static final String ALPHABET = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
/** BCrypt cost factor in the range [4, 31]. Default value is {@value}. */
private int cost = 12;
/** BCrypt version used when computing hashes. Default value is {@value}. */
private String version = "2b";
/** Creates a new instance. */
public BCryptHashBean() {}
/**
* Creates a new instance that uses the given cost factor when hashing.
*
* @param costFactor BCrypt cost in the range [4, 31].
*/
public BCryptHashBean(final int costFactor)
{
setCost(costFactor);
}
/**
* Sets the bcrypt cost factor.
*
* @param costFactor BCrypt cost in the range [4, 31].
*/
public void setCost(final int costFactor)
{
if (costFactor < 4 || costFactor > 31) {
throw new IllegalArgumentException("Cost must be in the range [4, 31].");
}
cost = costFactor;
}
/**
* Sets the bcrypt version.
*
* @param ver Bcrypt version, e.g. "2b"
*/
public void setVersion(final String ver)
{
if (!ver.startsWith("2") && ver.length() <= 2) {
throw new IllegalArgumentException("Invalid version: " + ver);
}
version = ver;
}
/**
* Compute a bcrypt hash of the form $2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
* given a salt and a password.
* @param data A 2-element array containing salt and password. The salt may be encoded per the bcrypt standard
* or raw bytes.
*
* @return An encoded bcrypt hash, yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
in the specification above.
*
* @throws CryptoException on bcrypt algorithm errors.
*/
@Override
public String hash(final Object... data) throws CryptoException
{
if (data.length != 2) {
throw new IllegalArgumentException("Expected exactly two elements in data array but got " + data.length);
}
return encode(BCrypt.generate(password(version, data[1]), salt(data[0]), cost), 23);
}
/**
* Compares a bcrypt hash of the form $2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
* with the computed hash from the given password. The bcrypt algorithm parameters are derived from the reference
* bcrypt hash string.
*
* @param data A 1-element array containing password.
*
* @return True if the computed hash is exactly equal to the reference hash, false otherwise.
*
* @throws CryptoException on bcrypt algorithm errors.
*/
@Override
public boolean compare(final String hash, final Object... data) throws CryptoException, StreamException
{
if (data.length != 1) {
throw new IllegalArgumentException("Expected exactly one element in data array but got " + data.length);
}
final BCryptParameters params = new BCryptParameters(hash);
final byte[] computed = BCrypt.generate(password(params.getVersion(), data[0]), params.getSalt(), params.getCost());
for (int i = 0; i < 23; i++) {
if (params.getHash()[i] != computed[i]) {
return false;
}
}
return true;
}
/**
* Encodes an input byte array into a string using the configured encoder.
*
* @param bytes Input bytes to encode.
* @param length Number of bytes of input to encode.
*
* @return Input encoded as a string.
*/
private static String encode(final byte[] bytes, final int length)
{
final Encoder encoder = new Base64Encoder.Builder().setAlphabet(ALPHABET).setPadding(false).build();
// Only want 184 bits (23 bytes) of the output
final ByteBuffer input = ByteBuffer.wrap(bytes, 0, length);
final CharBuffer output = CharBuffer.allocate(encoder.outputSize(length));
encoder.encode(input, output);
encoder.finalize(output);
return output.flip().toString();
}
/**
* Decodes an input string into a byte array using the configured decoder.
*
* @param input Input string to decode.
* @param length Desired output size in bytes.
*
* @return Input decoded as a byte array.
*/
private static byte[] decode(final String input, final int length)
{
final Decoder decoder = new Base64Decoder.Builder().setAlphabet(ALPHABET).setPadding(false).build();
final ByteBuffer output = ByteBuffer.allocate(decoder.outputSize(input.length()));
decoder.decode(CharBuffer.wrap(input), output);
decoder.finalize(output);
output.flip();
if (output.limit() != length) {
throw new IllegalArgumentException("Input is not of the expected size: " + output.limit() + "!=" + length);
}
return ByteUtil.toArray(output);
}
/**
* Converts an input object into a salt as an array of bytes.
*
* @param data Input salt as a byte array or encoded string.
*
* @return Salt as byte array.
*/
private static byte[] salt(final Object data)
{
if (data instanceof byte[]) {
return (byte[]) data;
} else if (data instanceof String) {
return decode((String) data, 16);
}
throw new IllegalArgumentException("Expected byte array or base-64 string.");
}
/**
* Converts an input object into a password as an array of UTF-8 bytes. A null terminator is added if the supplied
* data does not end with one.
*
* @param version Bcrypt version, e.g. "2a".
* @param data Input password.
*
* @return Null terminated password as UTF-8 byte array.
*/
private static byte[] password(final String version, final Object data)
{
if (data instanceof byte[]) {
final byte[] origData = (byte[]) data;
final byte[] newData;
if (origData[origData.length - 1] != 0x00) {
newData = new byte[origData.length + 1];
System.arraycopy(origData, 0, newData, 0, origData.length);
newData[newData.length - 1] = 0x00;
} else {
newData = origData;
}
return newData;
}
final StringBuilder sb = new StringBuilder();
if (data instanceof char[]) {
sb.append((char[]) data);
} else if (data instanceof String) {
sb.append((String) data);
} else {
throw new IllegalArgumentException("Expected byte array or string.");
}
if (sb.charAt(sb.length() - 1) != '\0') {
// Version 2a and later requires null terminator on password
sb.append('\0');
}
return sb.toString().getBytes(StandardCharsets.UTF_8);
}
/**
* Handles encoding and decoding a bcrypt hash of the form
* $2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
.
*/
public static class BCryptParameters
{
/** bcrypt version. */
private final String version;
/** bcrypt cost. */
private final int cost;
/** bcrypt salt. */
private final byte[] salt;
/** bcrypt hash. */
private final byte[] hash;
/**
* Decodes bcrypt parameters from a string.
*
* @param bCryptString bcrypt hash of the form
* $2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
*/
protected BCryptParameters(final String bCryptString)
{
if (!bCryptString.startsWith("$2")) {
throw new IllegalArgumentException("Expected bcrypt hash of the form $2n$cost$salthash");
}
final String[] parts = bCryptString.split("\\$");
if (parts.length != 4) {
throw new IllegalArgumentException("Invalid bcrypt hash");
}
version = parts[1];
cost = Integer.parseInt(parts[2]);
salt = decode(parts[3].substring(0, 22), 16);
hash = decode(parts[3].substring(22), 23);
}
/** @return bcrypt version. */
public String getVersion()
{
return version;
}
/** @return bcrypt cost in the range [4, 31]. */
public int getCost()
{
return cost;
}
/** @return bcrypt salt. */
public byte[] getSalt()
{
return salt;
}
/** @return bcrypt hash. */
public byte[] getHash()
{
return hash;
}
/**
* Produces an encoded bcrypt hash string from bcrypt parameter data.
*
* @return Bcrypt hash of the form $2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
.
*/
public String encode()
{
return new StringBuilder(60)
.append('$')
.append(version)
.append('$')
.append(cost)
.append('$')
.append(BCryptHashBean.encode(salt, 16))
.append(BCryptHashBean.encode(hash, 23))
.toString();
}
/**
* Produces an encoded bcrypt hash string from bcrypt parameters and a provided hash string.
*
* @param hash Encoded bcrypt hash bytes; e.g. the value produced from {@link #hash(Object...)}.
*
* @return Bcrypt hash of the form $2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
.
*/
public String encode(final String hash)
{
return new StringBuilder(60)
.append('$')
.append(version)
.append('$')
.append(cost)
.append('$')
.append(BCryptHashBean.encode(salt, 16))
.append(hash)
.toString();
}
}
}