Skip to content

Commit

Permalink
#99: bcrypt conversion to operation on bytes
Browse files Browse the repository at this point in the history
  • Loading branch information
firaja committed Feb 15, 2023
1 parent 1946e04 commit 10fce01
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 128 deletions.
248 changes: 135 additions & 113 deletions src/main/java/com/password4j/BcryptFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,124 @@ public static BcryptFunction getInstanceFromHash(String hashed)
}
}

@Override
public Hash hash(CharSequence plainTextPassword)
{
String salt = generateSalt();
return hash(plainTextPassword, salt);
}

public Hash hash(byte[] plainTextPasswordAsBytes)
{
String salt = generateSalt();
return internalHash(plainTextPasswordAsBytes, salt);
}

@Override
public Hash hash(CharSequence plainTextPassword, String salt)
{
return internalHash(plainTextPassword, salt);
}

public Hash hash(byte[] plainTextPassword, byte[] salt)
{
return internalHash(plainTextPassword, Utils.fromBytesToString(salt));
}

@Override
public boolean check(CharSequence plainTextPassword, String hashed)
{
return check(Utils.fromCharSequenceToBytes(plainTextPassword), Utils.fromCharSequenceToBytes(hashed));
}

public boolean check(byte[] plainTextPassword, byte[] hashed)
{
return internalCheck(plainTextPassword, hashed);
}

private Hash internalHash(CharSequence plainTextPassword, String salt)
{
byte[] passwordAsBytes = Utils.fromCharSequenceToBytes(plainTextPassword);
return internalHash(passwordAsBytes, salt);
}

protected Hash internalHash(byte[] plainTextPasswordAsBytes, String salt)
{
String realSalt;
byte[] saltAsBytes;
byte[] hashed;
char minor = (char) 0;
int off;
StringBuilder rs = new StringBuilder();

internalChecks(salt);

int saltLength = salt.length();

if (salt.charAt(2) == '$')
off = 3;
else
{
minor = salt.charAt(2);
if (isNotValidMinor(minor) || salt.charAt(3) != '$')
throw new BadParametersException("Invalid salt revision");
off = 4;
}

// Extract number of rounds
if (salt.charAt(off + 2) > '$')
throw new BadParametersException("Missing salt rounds");

if (off == 4 && saltLength < 29)
{
throw new BadParametersException("Invalid salt");
}

realSalt = salt.substring(off + 3, off + 25);
saltAsBytes = decodeBase64(realSalt, BCRYPT_SALT_LEN);

if (minor >= Bcrypt.A.minor()) // add null terminator
plainTextPasswordAsBytes = Arrays.copyOf(plainTextPasswordAsBytes, plainTextPasswordAsBytes.length + 1);

hashed = cryptRaw(plainTextPasswordAsBytes, saltAsBytes, logRounds, minor == Bcrypt.X.minor(), minor == Bcrypt.A.minor() ? 0x10000 : 0);

rs.append("$2");
if (minor >= Bcrypt.A.minor())
rs.append(minor);
rs.append('$');
if (logRounds < 10)
rs.append('0');
rs.append(logRounds);
rs.append('$');
encodeBase64(saltAsBytes, saltAsBytes.length, rs);
encodeBase64(hashed, BF_CRYPT_CIPHERTEXT.length * 4 - 1, rs);
String result = rs.toString();

return new Hash(this, result, hashed, saltAsBytes);
}

public int getLogarithmicRounds()
{
return logRounds;
}

public Bcrypt getType()
{
return type;
}

@Override
public String toString()
{
return getClass().getSimpleName() + '(' + toString(type, logRounds) + ')';
}

@Override
public int hashCode()
{
return Objects.hash(logRounds, type);
}

protected static String getUID(Bcrypt type, int logRounds)
{
return type.minor() + "|" + logRounds;
Expand Down Expand Up @@ -489,68 +607,9 @@ protected static String generateSalt(String prefix, int logRounds)
return rs.toString();
}

static boolean equalsNoEarlyReturn(String a, String b)
{
return MessageDigest.isEqual(Utils.fromCharSequenceToBytes(a), Utils.fromCharSequenceToBytes(b));
}

@Override
public Hash hash(CharSequence plainTextPassword)
{
String salt = generateSalt();
return hash(plainTextPassword, salt);
}

@Override
public Hash hash(CharSequence plainTextPassword, String salt)
{
return internalHash(plainTextPassword, salt);
}

@Override
public boolean check(CharSequence plainTextPassword, String hashed)
{
return checkPw(plainTextPassword, hashed);
}

private Hash internalHash(CharSequence plainTextPassword, String salt)
{
byte[] passwordAsBytes = Utils.fromCharSequenceToBytes(plainTextPassword);
return hash(passwordAsBytes, salt);
}

@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof BcryptFunction))
return false;
BcryptFunction that = (BcryptFunction) o;
return logRounds == that.logRounds && type == that.type;
}

public int getLogarithmicRounds()
{
return logRounds;
}

public Bcrypt getType()
{
return type;
}

@Override
public String toString()
{
return getClass().getSimpleName() + '(' + toString(type, logRounds) + ')';
}

@Override
public int hashCode()
{
return Objects.hash(logRounds, type);
}

/**
* Blowfish encipher a single 64-bit block encoded as
Expand Down Expand Up @@ -733,60 +792,18 @@ protected byte[] cryptRaw(byte[] password, byte[] salt, int logRounds, boolean s
return ret;
}

protected Hash hash(byte[] passwordb, String salt)
@Override
public boolean equals(Object o)
{
String realSalt;
byte[] saltb;
byte[] hashed;
char minor = (char) 0;
int off;
StringBuilder rs = new StringBuilder();

internalChecks(salt);

int saltLength = salt.length();

if (salt.charAt(2) == '$')
off = 3;
else
{
minor = salt.charAt(2);
if (isNotValidMinor(minor) || salt.charAt(3) != '$')
throw new BadParametersException("Invalid salt revision");
off = 4;
}

// Extract number of rounds
if (salt.charAt(off + 2) > '$')
throw new BadParametersException("Missing salt rounds");

if (off == 4 && saltLength < 29)
{
throw new BadParametersException("Invalid salt");
}

realSalt = salt.substring(off + 3, off + 25);
saltb = decodeBase64(realSalt, BCRYPT_SALT_LEN);

if (minor >= Bcrypt.A.minor()) // add null terminator
passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);

hashed = cryptRaw(passwordb, saltb, logRounds, minor == Bcrypt.X.minor(), minor == Bcrypt.A.minor() ? 0x10000 : 0);
if (this == o)
return true;
if (!(o instanceof BcryptFunction))
return false;
BcryptFunction that = (BcryptFunction) o;
return logRounds == that.logRounds && type == that.type;
}

rs.append("$2");
if (minor >= Bcrypt.A.minor())
rs.append(minor);
rs.append('$');
if (logRounds < 10)
rs.append('0');
rs.append(logRounds);
rs.append('$');
encodeBase64(saltb, saltb.length, rs);
encodeBase64(hashed, BF_CRYPT_CIPHERTEXT.length * 4 - 1, rs);
String result = rs.toString();

return new Hash(this, result, hashed, salt);
}

/**
* Generate a salt to be used with the {@link BcryptFunction#hash(CharSequence, String)} method
Expand All @@ -803,14 +820,19 @@ protected String generateSalt()
* Check that a plaintext password matches a previously hashed
* one
*
* @param plaintext the plaintext password to verify
* @param plainTextPasswordAsBytes the plaintext password to verify
* @param hashed the previously-hashed password
* @return true if the passwords match, false otherwise
* @since 0.1.0
*/
protected boolean checkPw(CharSequence plaintext, String hashed)
protected boolean internalCheck(byte[] plainTextPasswordAsBytes, byte[] hashed)
{
return equalsNoEarlyReturn(hashed, hash(plainTextPasswordAsBytes, hashed).getResultAsBytes());
}

static boolean equalsNoEarlyReturn(byte[] a, byte[] b)
{
return equalsNoEarlyReturn(hashed, hash(plaintext, hashed).getResult());
return MessageDigest.isEqual(a, b);
}

}
44 changes: 37 additions & 7 deletions src/main/java/com/password4j/Hash.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class Hash
* Depending on the implementation of the CHF, it may contain
* the salt and the configuration of the CHF itself.
*/
private String result;
private byte[] result;

/**
* Represents the computed output of a cryptographic hashing function.
Expand Down Expand Up @@ -123,12 +123,11 @@ private Hash()
*/
public Hash(HashingFunction hashingFunction, String result, byte[] bytes, String salt)
{
this.hashingFunction = hashingFunction;
this.salt = Utils.fromCharSequenceToBytes(salt);
this.result = result;
this.bytes = bytes;
this(hashingFunction, Utils.fromCharSequenceToBytes(result), bytes, Utils.fromCharSequenceToBytes(salt));
}



/**
* Constructs an {@link Hash} containing the basic information
* used and produced by the computational process of hashing a password.
Expand All @@ -145,6 +144,26 @@ public Hash(HashingFunction hashingFunction, String result, byte[] bytes, String
* @since 0.1.0
*/
public Hash(HashingFunction hashingFunction, String result, byte[] bytes, byte[] salt)
{
this(hashingFunction, Utils.fromCharSequenceToBytes(result), bytes, salt);
}

/**
* Constructs an {@link Hash} containing the basic information
* used and produced by the computational process of hashing a password.
* Other information, like <i>pepper</i> can be added with
* {@link #setPepper(CharSequence)}.
* <p>
* This constructor populates the object's attributes.
*
* @param hashingFunction the cryptographic algorithm used to produce the hash.
* @param result the result of the computation of the hash as bytes array.
* Notice that the format varies depending on the algorithm.
* @param bytes the hash without additional information.
* @param salt the salt used for the computation as bytes array.
* @since 1.7.0
*/
public Hash(HashingFunction hashingFunction, byte[] result, byte[] bytes, byte[] salt)
{
this.hashingFunction = hashingFunction;
this.salt = salt;
Expand All @@ -159,6 +178,17 @@ public Hash(HashingFunction hashingFunction, String result, byte[] bytes, byte[]
* @since 0.1.0
*/
public String getResult()
{
return Utils.fromBytesToString(result);
}

/**
* Retrieves the hash computed by the hashing function.
*
* @return the hash.
* @since 0.1.0
*/
public byte[] getResultAsBytes()
{
return result;
}
Expand Down Expand Up @@ -275,7 +305,7 @@ public boolean equals(Object obj)

private boolean hasSameValues(Hash otherHash)
{
return areEquals(this.result, otherHash.result) //
return Arrays.equals(this.result, otherHash.result) //
&& Arrays.equals(this.bytes, otherHash.bytes) //
&& Arrays.equals(this.salt, otherHash.salt) //
&& areEquals(this.pepper, otherHash.pepper) //
Expand All @@ -298,6 +328,6 @@ else if (cs1 != null && cs2 != null)
@Override
public int hashCode()
{
return Objects.hash(result, Arrays.hashCode(salt), pepper, hashingFunction);
return Objects.hash(Arrays.hashCode(result), Arrays.hashCode(salt), pepper, hashingFunction);
}
}
Loading

0 comments on commit 10fce01

Please sign in to comment.