Skip to content

Commit

Permalink
Add string out/in hash/verifier
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickfav committed Jul 12, 2018
1 parent 91cd184 commit 73fb0b9
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 26 deletions.
10 changes: 5 additions & 5 deletions README.md
Expand Up @@ -23,11 +23,10 @@ A simple example:

```java
String password = "1234";
char[] bcryptChars = BCrypt.withDefaults().hashToChar(12, password.toCharArray());
String bcryptHashString = new String(bcryptChars);
String bcryptHashString = BCrypt.withDefaults().hashToString(12, password.toCharArray());
// $2a$12$US00g/uMhoSBm.HiuieBjeMtoN69SN.GE25fCpldebzkryUyopws6
...
BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), bcryptChars);
BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString);
// result.verified == true
```

Expand All @@ -47,10 +46,11 @@ char[] bcryptChars = BCrypt.with(BCrypt.Version.VERSION_2B).hashToChar(6, passwo
```

By using `BCrypt.withDefaults()` it will default to version `$2a$`. The older `$2$` version is not supported.
For advanced use cases you may add your own version by providing a version identifier and a custom message formatter.
For advanced use cases you may add your own version by providing a version identifier and a custom message formatter
as well as parser.

```java
Version customVersion2f = new Version(new byte[]{0x32, 0x66} /* 2f */, myCustomFormatter);
Version customVersion2f = new Version(new byte[]{0x32, 0x66} /* 2f */, myCustomFormatter, myCustomParser);
```

### byte[] vs char[] API
Expand Down
68 changes: 53 additions & 15 deletions src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java
Expand Up @@ -129,13 +129,11 @@ public static final class Hasher {
private final Charset defaultCharset = DEFAULT_CHARSET;
private final Version version;
private final SecureRandom secureRandom;
private final Radix64Encoder encoder;
private final LongPasswordStrategy longPasswordStrategy;

private Hasher(Version version, SecureRandom secureRandom, LongPasswordStrategy longPasswordStrategy) {
this.version = version;
this.secureRandom = secureRandom;
this.encoder = new Radix64Encoder.Default();
this.longPasswordStrategy = longPasswordStrategy;
}

Expand All @@ -156,6 +154,25 @@ public char[] hashToChar(int cost, char[] password) {
return defaultCharset.decode(ByteBuffer.wrap(hash(cost, password))).array();
}

/**
* Hashes given password with the OpenBSD bcrypt schema. The cost factor will define how expensive the hash will
* be to generate. This method will use a {@link SecureRandom} to generate the internal 16 byte hash.
* <p>
* This implementation will add a null-terminator to the password and return a 23 byte length hash in accordance
* with the OpenBSD implementation.
* <p>
* The random salt will be created internally with a {@link SecureRandom} instance.
* <p>
* This is the same as calling <code>new String(hash(cost, password), StandardCharsets.UTF-8)</code>
*
* @param cost exponential cost (log2 factor) between {@link #MIN_COST} and {@link #MAX_COST} e.g. 12 --&gt; 2^12 = 4,096 iterations
* @param password to hash, will be internally converted to a utf-8 byte array representation
* @return bcrypt as utf-8 encoded String, which includes version, cost-factor, salt and the raw hash (as radix64)
*/
public String hashToString(int cost, char[] password) {
return new String(hash(cost, password), defaultCharset);
}

/**
* Hashes given password with the OpenBSD bcrypt schema. The cost factor will define how expensive the hash will
* be to generate. This method will use a {@link SecureRandom} to generate the internal 16 byte hash.
Expand Down Expand Up @@ -217,7 +234,7 @@ public byte[] hash(int cost, byte[] password) {
* @return bcrypt hash utf-8 encoded byte array which includes version, cost-factor, salt and the raw hash (as radix64)
*/
public byte[] hash(int cost, byte[] salt, byte[] password) {
return version.bCryptFormatter.createHashMessage(hashRaw(cost, salt, password));
return version.formatter.createHashMessage(hashRaw(cost, salt, password));
}

/**
Expand Down Expand Up @@ -330,10 +347,8 @@ public int hashCode() {
*/
public static final class Verifyer {
private final Charset defaultCharset = DEFAULT_CHARSET;
private final Radix64Encoder encoder;

private Verifyer() {
this.encoder = new Radix64Encoder.Default();
}

/**
Expand Down Expand Up @@ -396,6 +411,22 @@ public Result verify(char[] password, char[] bcryptHash) {
return verify(password, bcryptHash, null);
}

/**
* Verify given bcrypt hash, which includes salt and cost factor with given raw password.
* The result will have {@link Result#verified} true if they match. If given hash has an
* invalid format {@link Result#validFormat} will be false; see also {@link Result#formatErrorMessage}
* for easier debugging.
* <p>
* Same as calling <code>verify(password, bcryptHash.toCharArray())</code>
*
* @param password to compare against the hash
* @param bcryptHash to compare against the password
* @return result object, see {@link Result} for more info
*/
public Result verify(char[] password, String bcryptHash) {
return verify(password, bcryptHash.toCharArray(), null);
}

private Result verify(char[] password, char[] bcryptHash, Version requiredVersion) {
byte[] passwordBytes = null;
byte[] bcryptHashBytes = null;
Expand All @@ -419,7 +450,7 @@ private Result verify(char[] password, char[] bcryptHash, Version requiredVersio
private Result verify(byte[] password, byte[] bcryptHash, Version requiredVersion) {
Objects.requireNonNull(bcryptHash);

BCryptParser parser = new BCryptParser.Default(encoder, defaultCharset);
BCryptParser parser = requiredVersion == null ? Version.VERSION_2A.parser : requiredVersion.parser;
try {
HashData hashData = parser.parse(bcryptHash);

Expand Down Expand Up @@ -549,7 +580,8 @@ public String toString() {
* See: https://passlib.readthedocs.io/en/stable/modular_crypt_format.html
*/
public static final class Version {
private static final BCryptFormatter formatter = new BCryptFormatter.Default(new Radix64Encoder.Default(), BCrypt.DEFAULT_CHARSET);
private static final BCryptFormatter DEFAULT_FORMATTER = new BCryptFormatter.Default(new Radix64Encoder.Default(), BCrypt.DEFAULT_CHARSET);
private static final BCryptParser DEFAULT_PARSER = new BCryptParser.Default(new Radix64Encoder.Default(), BCrypt.DEFAULT_CHARSET);

/**
* $2a$
Expand All @@ -559,7 +591,7 @@ public static final class Version {
* - the string must be UTF-8 encoded
* - the null terminator must be included
*/
public static final Version VERSION_2A = new Version(new byte[]{MAJOR_VERSION, 0x61}, formatter);
public static final Version VERSION_2A = new Version(new byte[]{MAJOR_VERSION, 0x61}, DEFAULT_FORMATTER, DEFAULT_PARSER);

/**
* $2b$ (2014/02)
Expand All @@ -568,7 +600,7 @@ public static final class Version {
* in an unsigned char (i.e. 8-bit Byte). If a password was longer than 255 characters, it would overflow
* and wrap at 255. To recognize possible incorrect hashes, a new version was created.
*/
public static final Version VERSION_2B = new Version(new byte[]{MAJOR_VERSION, 0x62}, formatter);
public static final Version VERSION_2B = new Version(new byte[]{MAJOR_VERSION, 0x62}, DEFAULT_FORMATTER, DEFAULT_PARSER);

/**
* $2x$ (2011)
Expand All @@ -580,14 +612,14 @@ public static final class Version {
* Nobody else, including canonical OpenBSD, adopted the idea of 2x/2y. This version marker change was limited
* to crypt_blowfish.
*/
public static final Version VERSION_2X = new Version(new byte[]{MAJOR_VERSION, 0x78}, formatter);
public static final Version VERSION_2X = new Version(new byte[]{MAJOR_VERSION, 0x78}, DEFAULT_FORMATTER, DEFAULT_PARSER);

/**
* $2y$ (2011)
* <p>
* See {@link #VERSION_2X}
*/
public static final Version VERSION_2Y = new Version(new byte[]{MAJOR_VERSION, 0x79}, formatter);
public static final Version VERSION_2Y = new Version(new byte[]{MAJOR_VERSION, 0x79}, DEFAULT_FORMATTER, DEFAULT_PARSER);

/**
* List of supported versions
Expand All @@ -601,18 +633,24 @@ public static final class Version {
/**
* The formatter for the bcrypt message digest
*/
public final BCryptFormatter bCryptFormatter;
public final BCryptFormatter formatter;

/**
* The parser used to parse a bcrypt message
*/
public final BCryptParser parser;

/**
* Create a new version. Only use this if you are know what you are doing, most common versions are already available with
* {@link Version#VERSION_2A}, {@link Version#VERSION_2Y} etc.
*
* @param versionIdentifier version as UTF-8 encoded byte array, e.g. '2a' = new byte[]{0x32, 0x61}, do not included the separator '$'
* @param bCryptFormatter the formatter responsible for formatting the out hash message digest
* @param formatter the formatter responsible for formatting the out hash message digest
*/
public Version(byte[] versionIdentifier, BCryptFormatter bCryptFormatter) {
public Version(byte[] versionIdentifier, BCryptFormatter formatter, BCryptParser parser) {
this.versionIdentifier = versionIdentifier;
this.bCryptFormatter = bCryptFormatter;
this.formatter = formatter;
this.parser = parser;
}

@Override
Expand Down
13 changes: 7 additions & 6 deletions src/test/java/at/favre/lib/crypto/bcrypt/BcryptTest.java
Expand Up @@ -41,10 +41,9 @@ public void testEntriesAgainstRef() {
@Test
public void quickStart() {
String password = "1234";
char[] bcryptChars = BCrypt.withDefaults().hashToChar(12, password.toCharArray());
String bcryptHashString = new String(bcryptChars);
String bcryptHashString = BCrypt.withDefaults().hashToString(12, password.toCharArray());
System.out.println(bcryptHashString);
BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), bcryptChars);
BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString);
assertTrue(result.verified);
}

Expand Down Expand Up @@ -112,11 +111,13 @@ private void checkHash(BCrypt.Hasher bCrypt) throws Exception {
byte[] hash2 = bCrypt.hash(7, salt, pw.getBytes(UTF_8));
BCrypt.HashData hashData = bCrypt.hashRaw(7, salt, pw.getBytes(UTF_8));
char[] hash3 = bCrypt.hashToChar(4, pw.toCharArray());
String hash4 = bCrypt.hashToString(4, pw.toCharArray());

assertFalse(Bytes.wrap(hash1).equals(hash2));
assertTrue(verifyer.verify(pw.toCharArray(), new String(hash1, UTF_8).toCharArray()).verified);
assertTrue(verifyer.verify(pw.getBytes(UTF_8), hash2).verified);
assertTrue(verifyer.verify(pw.toCharArray(), hash3).verified);
assertTrue(verifyer.verify(pw.toCharArray(), hash4).verified);
assertEquals(new BCryptParser.Default(new Radix64Encoder.Default(), UTF_8).parse(hash2), hashData);
}

Expand Down Expand Up @@ -343,14 +344,14 @@ public void testHashDataWipe() {
@Test
public void testVersionPojoMethods() {
assertEquals(BCrypt.Version.VERSION_2A, BCrypt.Version.VERSION_2A);
assertEquals(BCrypt.Version.VERSION_2A, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x61}, null));
assertEquals(BCrypt.Version.VERSION_2Y, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x79}, null));
assertEquals(BCrypt.Version.VERSION_2A, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x61}, null, null));
assertEquals(BCrypt.Version.VERSION_2Y, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x79}, null, null));
assertNotEquals(BCrypt.Version.VERSION_2Y, BCrypt.Version.VERSION_2A);
assertNotEquals(BCrypt.Version.VERSION_2A, BCrypt.Version.VERSION_2B);
assertNotEquals(BCrypt.Version.VERSION_2X, BCrypt.Version.VERSION_2Y);

assertEquals(BCrypt.Version.VERSION_2A.hashCode(), BCrypt.Version.VERSION_2A.hashCode());
assertEquals(BCrypt.Version.VERSION_2A.hashCode(), new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x61}, null).hashCode());
assertEquals(BCrypt.Version.VERSION_2A.hashCode(), new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x61}, null, null).hashCode());

assertNotEquals(BCrypt.Version.VERSION_2Y.hashCode(), BCrypt.Version.VERSION_2A.hashCode());
assertNotEquals(BCrypt.Version.VERSION_2A.hashCode(), BCrypt.Version.VERSION_2B.hashCode());
Expand Down

0 comments on commit 73fb0b9

Please sign in to comment.