Skip to content

Commit

Permalink
Added parser and verify
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickfav committed Jul 9, 2018
1 parent 1aad3e9 commit 3697470
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 70 deletions.
108 changes: 69 additions & 39 deletions src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java
Expand Up @@ -7,6 +7,7 @@
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Objects;

public final class BCrypt {
/**
Expand All @@ -19,7 +20,7 @@ public final class BCrypt {
*/
static final byte MAJOR_VERSION = 0x32;
static final int SALT_LENGTH = 16;
static final int MAX_PW_LENGTH_BYTE = 71;
static final int MAX_PW_LENGTH_BYTE = 72;
static final int MIN_COST = 4;
static final int MAX_COST = 30;

Expand Down Expand Up @@ -53,7 +54,16 @@ private BCrypt(Version version, SecureRandom secureRandom, BCryptProtocol.Encode
}

public byte[] hash(int cost, char[] password) {
return hash(cost, generateRandomSalt(), password);
byte[] passwordBytes = null;
try {
passwordBytes = new String(CharBuffer.allocate(password.length + 1).put(password).array())
.getBytes(defaultCharset);
return hash(cost, generateRandomSalt(), passwordBytes);
} finally {
if (passwordBytes != null) {
Bytes.wrap(passwordBytes).mutable().secureWipe();
}
}
}

private byte[] generateRandomSalt() {
Expand All @@ -62,8 +72,7 @@ private byte[] generateRandomSalt() {
return salt;
}

public byte[] hash(int cost, byte[] salt, char[] password) {

byte[] hash(int cost, byte[] salt, byte[] password) {
if (cost > MAX_COST || cost < MIN_COST) {
throw new IllegalArgumentException("cost factor must be between " + MIN_COST + " and " + MAX_COST + ", was " + cost);
}
Expand All @@ -76,13 +85,17 @@ public byte[] hash(int cost, byte[] salt, char[] password) {
if (password == null) {
throw new IllegalArgumentException("provided password must not be null");
}
if (password.length > MAX_PW_LENGTH_BYTE || password.length < 0) {
throw new IllegalArgumentException("password must be between 1 and 72 bytes encoded in utf-8, was " + password.length);
if (password.length > MAX_PW_LENGTH_BYTE) {
throw new IllegalArgumentException("password must be between 0 and 72 bytes encoded in utf-8, was " + password.length);
}

byte[] hash = new BCryptProtocol.BcryptHasher().cryptRaw(cost, salt, password, defaultCharset);

return createOutMessage(cost, salt, hash);
byte[] pwWithNullTerminator = password = Bytes.wrap(password).append((byte) 0).array();
try {
byte[] hash = new BCryptProtocol.BcryptHasher().cryptRaw(cost, salt, password);
return createOutMessage(cost, salt, hash);
} finally {
Bytes.wrap(pwWithNullTerminator).mutable().secureWipe();
}
}

private byte[] createOutMessage(int cost, byte[] salt, byte[] hash) {
Expand All @@ -105,38 +118,47 @@ private byte[] createOutMessage(int cost, byte[] salt, byte[] hash) {
}
}

public boolean verify(char[] bcryptHash) {
return verifyWithResult(bcryptHash).verified;
public Result verifyStrict(char[] password, char[] bcryptHash) {
return verify(password, bcryptHash, true);
}

public Result verifyWithResult(char[] bcryptHash) {
if (bcryptHash == null || bcryptHash.length == 0) {
throw new IllegalArgumentException("must provide non-null, non-empty hash");
}

byte[] hashBytes = defaultCharset.encode(CharBuffer.wrap(bcryptHash)).array();
public Result verify(char[] password, char[] bcryptHash) {
return verify(password, bcryptHash, false);
}

if (hashBytes.length < 7) {
throw new IllegalBCryptFormatException("hash prefix meta must be at least 6 bytes long e.g. '$2a$10$'");
private Result verify(char[] password, char[] bcryptHash, boolean strictVersion) {
byte[] passwordBytes = null;
byte[] bcryptHashBytes = null;
try {
passwordBytes = new String(CharBuffer.allocate(password.length + 1).put(password).array()).getBytes(defaultCharset);
bcryptHashBytes = new String(CharBuffer.allocate(bcryptHash.length + 1).put(bcryptHash).array()).getBytes(defaultCharset);
return verify(passwordBytes, bcryptHashBytes, strictVersion);
} finally {
if (passwordBytes != null) {
Bytes.wrap(passwordBytes).mutable().secureWipe();
}
if (bcryptHashBytes != null) {
Bytes.wrap(bcryptHashBytes).mutable().secureWipe();
}
}
}

if (hashBytes[0] != SEPARATOR || hashBytes[1] != MAJOR_VERSION) {
throw new IllegalBCryptFormatException("hash must start with " + new String(new byte[]{SEPARATOR, MAJOR_VERSION}));
}
public Result verify(byte[] password, byte[] bcryptHash, boolean strictVersion) {
Objects.requireNonNull(bcryptHash);

BCryptParser parser = new BCryptParser.Default(defaultCharset, encoder);
try {
BCryptParser.Parts parts = parser.parse(bcryptHash);

Version usedVersion = null;
for (Version versionToTest : Version.values()) {
if (versionToTest.test(hashBytes, true)) {
usedVersion = versionToTest;
break;
if (strictVersion && parts.version != version) {
return new Result(parts, false);
}
}

if (usedVersion == null) {
throw new IllegalBCryptFormatException("unknown bcrypt version");
byte[] refHash = BCrypt.with(parts.version).hash(parts.cost, parts.salt, password);
return new Result(parts, Bytes.wrap(refHash).equals(bcryptHash));
} catch (IllegalBCryptFormatException e) {
return new Result(e);
}

return new Result(usedVersion, 0, null, false);
}

/**
Expand All @@ -152,16 +174,24 @@ public byte[] upgrade(char[] currentLowercostBcryptHash, int newMinCost) {
}

public static final class Result {
public final Version version;
public final int cost;
public final byte[] salt;
public final BCryptParser.Parts details;
public final boolean validFormat;
public final boolean verified;
public final String formatErrorMessage;

public Result(IllegalBCryptFormatException e) {
this(null, false, false, e.getMessage());
}

public Result(BCryptParser.Parts details, boolean verified) {
this(details, true, verified, null);
}

public Result(Version version, int cost, byte[] salt, boolean verified) {
this.version = version;
this.cost = cost;
this.salt = salt;
private Result(BCryptParser.Parts details, boolean validFormat, boolean verified, String formatErrorMessage) {
this.details = details;
this.validFormat = validFormat;
this.verified = verified;
this.formatErrorMessage = formatErrorMessage;
}
}

Expand Down
68 changes: 56 additions & 12 deletions src/main/java/at/favre/lib/crypto/bcrypt/BCryptParser.java
@@ -1,30 +1,36 @@
package at.favre.lib.crypto.bcrypt;

import at.favre.lib.bytes.Bytes;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Objects;

import static at.favre.lib.crypto.bcrypt.BCrypt.MAJOR_VERSION;
import static at.favre.lib.crypto.bcrypt.BCrypt.SEPARATOR;

public interface BCryptParser {

Parts parse(byte[] bcryptHash);
Parts parse(byte[] bcryptHash) throws IllegalBCryptFormatException;

final class Default implements BCryptParser {

public final Charset defaultCharset;
private final Charset defaultCharset;
private final BCryptProtocol.Encoder encoder;

public Default(Charset defaultCharset) {
Default(Charset defaultCharset, BCryptProtocol.Encoder encoder) {
this.defaultCharset = defaultCharset;
this.encoder = encoder;
}

@Override
public Parts parse(byte[] bcryptHash) {
public Parts parse(byte[] bcryptHash) throws IllegalBCryptFormatException {
if (bcryptHash == null || bcryptHash.length == 0) {
throw new IllegalArgumentException("must provide non-null, non-empty hash");
}

if (bcryptHash.length < 7) {
throw new IllegalBCryptFormatException("hash prefix meta must be at least 6 bytes long e.g. '$2a$10$'");
throw new IllegalBCryptFormatException("hash prefix meta must be at least 7 bytes long e.g. '$2a$10$'");
}

if (bcryptHash[0] != SEPARATOR || bcryptHash[1] != MAJOR_VERSION) {
Expand All @@ -44,8 +50,8 @@ public Parts parse(byte[] bcryptHash) {
}

byte[] costBytes = new byte[2];
costBytes[0] = bcryptHash[usedVersion.versionPrefix.length - 1];
costBytes[1] = bcryptHash[usedVersion.versionPrefix.length];
costBytes[0] = bcryptHash[usedVersion.versionPrefix.length];
costBytes[1] = bcryptHash[usedVersion.versionPrefix.length + 1];

int parsedCostFactor;
try {
Expand All @@ -54,15 +60,23 @@ public Parts parse(byte[] bcryptHash) {
throw new IllegalBCryptFormatException("cannot parse cost factor '" + new String(costBytes, defaultCharset) + "'");
}

if (bcryptHash[usedVersion.versionPrefix.length + 1] != SEPARATOR) {
throw new IllegalBCryptFormatException("expected separator " + SEPARATOR + " after cost factor");
if (bcryptHash[usedVersion.versionPrefix.length + 2] != SEPARATOR) {
throw new IllegalBCryptFormatException("expected separator " + Bytes.from(SEPARATOR).encodeUtf8() + " after cost factor");
}

if (bcryptHash.length != 7 + 22 + 31) {
throw new IllegalBCryptFormatException("hash expected to be exactly 60 bytes");
}

/*for (int i = ; i <; i++) {
byte[] salt = new byte[22];
byte[] hash = new byte[31];

}*/
System.arraycopy(bcryptHash, 7, salt, 0, salt.length);
System.arraycopy(bcryptHash, 7 + salt.length, hash, 0, hash.length);

return new Parts(usedVersion, parsedCostFactor, null, null);
return new Parts(usedVersion, parsedCostFactor,
encoder.decode(new String(salt), salt.length),
encoder.decode(new String(hash), hash.length));
}
}

Expand All @@ -78,5 +92,35 @@ final class Parts {
this.salt = salt;
this.hash = hash;
}

@Override
public String toString() {
return "Parts{" +
"version=" + version +
", cost=" + cost +
", salt=" + Bytes.wrap(salt).toString() +
", hash=" + Bytes.wrap(hash).toString() +
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Parts parts = (Parts) o;
return cost == parts.cost &&
version == parts.version &&
Arrays.equals(salt, parts.salt) &&
Arrays.equals(hash, parts.hash);
}

@Override
public int hashCode() {

int result = Objects.hash(version, cost);
result = 31 * result + Arrays.hashCode(salt);
result = 31 * result + Arrays.hashCode(hash);
return result;
}
}
}
18 changes: 3 additions & 15 deletions src/main/java/at/favre/lib/crypto/bcrypt/BCryptProtocol.java
@@ -1,9 +1,6 @@
package at.favre.lib.crypto.bcrypt;

import java.io.ByteArrayOutputStream;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;

final class BCryptProtocol {

Expand Down Expand Up @@ -105,7 +102,7 @@ private static byte char64(char x) {
* byte array. Note that this is *not* compatible with
* the standard MIME-base64 encoding.
*
* @param s the string to decode
* @param s the string to decode
* @param maxLen the maximum number of bytes to decode
* @return an array containing the decoded bytes
* @throws IllegalArgumentException if maxolen is invalid
Expand Down Expand Up @@ -434,17 +431,8 @@ static final class BcryptHasher {
BcryptHasher() {
}

byte[] cryptRaw(int cost, byte[] salt, char[] password, Charset charset) {
byte[] passwordBytes = null;
try {
passwordBytes = new String(CharBuffer.allocate(password.length + 1).put(password).put("\000").array())
.getBytes(charset);
return cryptRaw(cost, salt, passwordBytes, bf_crypt_ciphertext.clone());
} finally {
if (passwordBytes != null) {
Arrays.fill(passwordBytes, (byte) 0);
}
}
byte[] cryptRaw(int cost, byte[] salt, byte[] password) {
return cryptRaw(cost, salt, password, bf_crypt_ciphertext.clone());
}

/**
Expand Down
@@ -1,6 +1,6 @@
package at.favre.lib.crypto.bcrypt;

public class IllegalBCryptFormatException extends IllegalArgumentException {
public class IllegalBCryptFormatException extends Exception {

public IllegalBCryptFormatException(String s) {
super(s);
Expand All @@ -13,4 +13,10 @@ public IllegalBCryptFormatException(String message, Throwable cause) {
public IllegalBCryptFormatException(Throwable cause) {
super(cause);
}

@Override
public String getMessage() {
return super.getMessage() + " - example of expected hash format: '$2a$06$If6bvum7DFjUnE9p2uDeDu0YHzrHM6tf.iqN8.yx.jNN1ILEf7h0i'" +
" which includes 16 bytes salt and 23 bytes hash value encoded in a base64 flavor";
}
}

0 comments on commit 3697470

Please sign in to comment.