Skip to content

Commit

Permalink
Add flags to Version so that bcrypt can output 24 byte and make null-…
Browse files Browse the repository at this point in the history
…term optional

fixes #3
  • Loading branch information
patrickfav committed Jul 13, 2018
1 parent 7325307 commit 69bd1d9
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 20 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
@@ -1,5 +1,9 @@
# Releases

## v0.3.0

* support 24byte hash out and make null terminator optional (BC style) #3

## v0.2.0

* add String API
Expand Down
58 changes: 49 additions & 9 deletions src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java
Expand Up @@ -264,14 +264,22 @@ public HashData hashRaw(int cost, byte[] salt, byte[] password) {
if (password == null) {
throw new IllegalArgumentException("provided password must not be null");
}
if (password.length > MAX_PW_LENGTH_BYTE) {

if (!version.appendNullTerminator && password.length == 0) {
throw new IllegalArgumentException("provided password must at least be length 1 if no null terminator is appended");
}

if (password.length > MAX_PW_LENGTH_BYTE + (version.appendNullTerminator ? 0 : 1)) {
password = longPasswordStrategy.derive(password);
}

byte[] pwWithNullTerminator = Bytes.wrap(password).append((byte) 0).array();
byte[] pwWithNullTerminator = version.appendNullTerminator ? Bytes.wrap(password).append((byte) 0).array() : Bytes.wrap(password).copy().array();
try {
byte[] hash = new BCryptOpenBSDProtocol().cryptRaw(1 << cost, salt, pwWithNullTerminator);
return new HashData(cost, version, salt, Bytes.wrap(hash).resize(HASH_OUT_LENGTH, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array());
return new HashData(cost, version, salt, version.useOnly23bytesForHash ?
Bytes.wrap(hash).resize(HASH_OUT_LENGTH, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array() :
hash
);
} finally {
Bytes.wrap(pwWithNullTerminator).mutable().secureWipe();
}
Expand Down Expand Up @@ -304,7 +312,7 @@ public HashData(int cost, Version version, byte[] rawSalt, byte[] rawHash) {
Objects.requireNonNull(rawSalt);
Objects.requireNonNull(version);
if (!Bytes.wrap(rawSalt).validate(BytesValidators.exactLength(16)) ||
!Bytes.wrap(rawHash).validate(BytesValidators.or(BytesValidators.exactLength(23)))) {
!Bytes.wrap(rawHash).validate(BytesValidators.or(BytesValidators.exactLength(23), BytesValidators.exactLength(24)))) {
throw new IllegalArgumentException("salt must be exactly 16 bytes and hash 23 bytes long");
}
this.cost = cost;
Expand Down Expand Up @@ -621,6 +629,12 @@ public static final class Version {
*/
public static final Version VERSION_2Y = new Version(new byte[]{MAJOR_VERSION, 0x79}, DEFAULT_FORMATTER, DEFAULT_PARSER);

/**
* This mirrors how Bouncy Castle creates bcrypt hashes: with 24 byte out and without null-terminator. Gets a fake
* version descriptor.
*/
public static final Version VERSION_BC = new Version(new byte[]{MAJOR_VERSION, 0x63}, false, false, DEFAULT_FORMATTER, DEFAULT_PARSER);

/**
* List of supported versions
*/
Expand All @@ -630,6 +644,19 @@ public static final class Version {
* Version identifier byte array, eg.{0x32, 0x61} for '2a'
*/
public final byte[] versionIdentifier;

/**
* Due to a bug the OpenBSD implemenation only uses 23 bytes (184 bit) of the possible 24 byte output from
* blowfish. Set this to false if you want the full 24 byte out (which makes it incompatible with most other impl)
*/
public final boolean useOnly23bytesForHash;

/**
* Since OpenBSD bcrypt version $2a$ a null-terminator byte must be append to the hash. This flag decides if
* that will be done during hashing.
*/
public final boolean appendNullTerminator;

/**
* The formatter for the bcrypt message digest
*/
Expand All @@ -640,15 +667,23 @@ public static final class Version {
*/
public final BCryptParser parser;

private Version(byte[] versionIdentifier, BCryptFormatter formatter, BCryptParser parser) {
this(versionIdentifier, true, true, formatter, 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 formatter the formatter responsible for formatting the out hash message digest
* @param versionIdentifier version as UTF-8 encoded byte array, e.g. '2a' = new byte[]{0x32, 0x61}, do not included the separator '$'
* @param useOnly23bytesForHash set to false if you want the full 24 byte out for the hash (otherwise will be truncated to 23 byte according to OpenBSD impl)
* @param appendNullTerminator as defined in $2a$+ a null terminator is appended to the password, pass false if you want avoid this
* @param formatter the formatter responsible for formatting the out hash message digest
*/
public Version(byte[] versionIdentifier, BCryptFormatter formatter, BCryptParser parser) {
public Version(byte[] versionIdentifier, boolean useOnly23bytesForHash, boolean appendNullTerminator, BCryptFormatter formatter, BCryptParser parser) {
this.versionIdentifier = versionIdentifier;
this.useOnly23bytesForHash = useOnly23bytesForHash;
this.appendNullTerminator = appendNullTerminator;
this.formatter = formatter;
this.parser = parser;
}
Expand All @@ -658,12 +693,17 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Version version = (Version) o;
return Arrays.equals(versionIdentifier, version.versionIdentifier);
return useOnly23bytesForHash == version.useOnly23bytesForHash &&
appendNullTerminator == version.appendNullTerminator &&
Arrays.equals(versionIdentifier, version.versionIdentifier);
}

@Override
public int hashCode() {
return Arrays.hashCode(versionIdentifier);

int result = Objects.hash(useOnly23bytesForHash, appendNullTerminator);
result = 31 * result + Arrays.hashCode(versionIdentifier);
return result;
}

@Override
Expand Down
36 changes: 28 additions & 8 deletions src/test/java/at/favre/lib/crypto/bcrypt/BcBcryptTestCases.java
@@ -1,13 +1,11 @@
package at.favre.lib.crypto.bcrypt;

import at.favre.lib.bytes.Bytes;
import at.favre.lib.bytes.BytesTransformer;
import at.favre.lib.crypto.bcrypt.misc.Repeat;
import at.favre.lib.crypto.bcrypt.misc.RepeatRule;
import org.junit.Rule;
import org.junit.Test;

import java.nio.charset.StandardCharsets;
import java.util.Random;

import static at.favre.lib.crypto.bcrypt.BcryptTest.UTF_8;
Expand All @@ -22,19 +20,41 @@ public class BcBcryptTestCases {
@Rule
public RepeatRule repeatRule = new RepeatRule();

// see: https://www.programcreek.com/java-api-examples/?code=ttt43ttt/gwt-crypto/gwt-crypto-master/src/test/java/org/bouncycastle/crypto/test/BCryptTest.java
private static final Object[][] testVectors = {
{"00", "144b3d691a7b4ecf39cf735c7fa7a79c", 6, "557e94f34bf286e8719a26be94ac1e16d95ef9f819dee092"},
{"00", "26c63033c04f8bcba2fe24b574db6274", 8, "56701b26164d8f1bc15225f46234ac8ac79bf5bc16bf48ba"},
{"00", "9b7c9d2ada0fd07091c915d1517701d6", 10, "7b2e03106a43c9753821db688b5cc7590b18fdf9ba544632"},
{"6100", "a3612d8c9a37dac2f99d94da03bd4521", 6, "e6d53831f82060dc08a2e8489ce850ce48fbf976978738f3"},
{"6100", "7a17b15dfe1c4be10ec6a3ab47818386", 8, "a9f3469a61cbff0a0f1a1445dfe023587f38b2c9c40570e1"},
{"6100", "9bef4d04e1f8f92f3de57323f8179190", 10, "5169fd39606d630524285147734b4c981def0ee512c3ace1"},
{"61626300", "2a1f1dc70a3d147956a46febe3016017", 6, "d9a275b493bcbe1024b0ff80d330253cfdca34687d8f69e5"},
{"61626300", "4ead845a142c9bc79918c8797f470ef5", 8, "8d4131a723bfbbac8a67f2e035cae08cc33b69f37331ea91"},
{"61626300", "631c554493327c32f9c26d9be7d18e4c", 10, "8cd0b863c3ff0860e31a2b42427974e0283b3af7142969a6"}};

@Test
@Repeat(10)
public void testRandomAgainstJBcrypt() throws IllegalBCryptFormatException {
public void testRandomAgainstJBcrypt() {
int cost = new Random().nextInt(3) + 4;
String pw = Bytes.random(8 + new Random().nextInt(24)).encodeBase64();
byte[] salt = Bytes.random(16).array();

//BC will only return the hash without the salt, cost factor and version identifier and does not add a null terminator
byte[] bcryptHashOnly = org.bouncycastle.crypto.generators.BCrypt.generate(Bytes.from(pw).append((byte) 0).array(), salt, cost);

byte[] hash = BCrypt.with(BCrypt.Version.VERSION_2A).hash(cost, salt, pw.getBytes(UTF_8));
BCrypt.HashData parts = new BCryptParser.Default(new Radix64Encoder.Default(), StandardCharsets.UTF_8).parse(hash);
byte[] bcryptHashOnly = org.bouncycastle.crypto.generators.BCrypt.generate(Bytes.from(pw).array(), salt, cost);
BCrypt.HashData hash = BCrypt.with(BCrypt.Version.VERSION_BC).hashRaw(cost, salt, pw.getBytes(UTF_8));
assertArrayEquals(hash.rawHash, bcryptHashOnly);
}

assertArrayEquals(parts.rawHash, Bytes.wrap(bcryptHashOnly).resize(23, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array());
@Test
public void testBcRefVectors() {
for (Object[] testVector : testVectors) {
byte[] pw = Bytes.parseHex((String) testVector[0]).array();
byte[] salt = Bytes.parseHex((String) testVector[1]).array();
int cost = (int) testVector[2];
byte[] refHash = Bytes.parseHex((String) testVector[3]).array();

BCrypt.HashData hash = BCrypt.with(BCrypt.Version.VERSION_BC).hashRaw(cost, salt, pw);
assertArrayEquals(refHash, hash.rawHash);
}
}
}
18 changes: 15 additions & 3 deletions src/test/java/at/favre/lib/crypto/bcrypt/BcryptTest.java
Expand Up @@ -130,6 +130,13 @@ public void hashRandomByteArrays() {
System.out.println(Bytes.wrap(hash).encodeUtf8());
}

@Test
public void testEmptyPw() {
byte[] hash = BCrypt.with(BCrypt.Version.VERSION_2A).hash(4, new byte[0]);
assertTrue(BCrypt.verifyer().verify(new byte[0], hash).verified);
System.out.println(Bytes.wrap(hash).encodeUtf8());
}

@Test(expected = IllegalArgumentException.class)
public void createHashCostTooSmall() {
BCrypt.withDefaults().hash(3, "123".toCharArray());
Expand Down Expand Up @@ -160,6 +167,11 @@ public void createHashWithPwNull() {
BCrypt.withDefaults().hash(6, new byte[16], null);
}

@Test(expected = IllegalArgumentException.class)
public void createHashWithPwEmptyNoNullTerm() {
BCrypt.with(BCrypt.Version.VERSION_BC).hash(6, new byte[16], new byte[0]);
}

@Test(expected = IllegalArgumentException.class)
public void createHashWithCharPwNull() {
BCrypt.withDefaults().hash(6, (char[]) null);
Expand Down Expand Up @@ -344,14 +356,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, null));
assertEquals(BCrypt.Version.VERSION_2Y, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x79}, null, null));
assertEquals(BCrypt.Version.VERSION_2A, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x61}, true, true, null, null));
assertEquals(BCrypt.Version.VERSION_2Y, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x79}, true, true, 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, null).hashCode());
assertEquals(BCrypt.Version.VERSION_2A.hashCode(), new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x61}, true, true, 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 69bd1d9

Please sign in to comment.