From 994c8c6d95014625d250921d54ee635d05283172 Mon Sep 17 00:00:00 2001 From: Patrick Favre-Bulle Date: Mon, 21 Oct 2019 23:15:33 +0200 Subject: [PATCH 1/3] Allow for custom max password length in 'Version' #22 --- CHANGELOG | 3 + README.md | 4 +- .../at/favre/lib/crypto/bcrypt/BCrypt.java | 155 ++++++++++++------ .../crypto/bcrypt/LongPasswordStrategies.java | 15 +- .../favre/lib/crypto/bcrypt/BcryptTest.java | 98 +++++++---- .../bcrypt/LongPasswordStrategyTest.java | 7 +- 6 files changed, 192 insertions(+), 90 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d20c9f1..88d9d77 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,10 +4,13 @@ * fix license headers and correct credits to jBcrypt * add long-password strategy to verifier #21 * fix not returning correct hash version when verifying #24 +* allow for custom max password length in Version #22 ### Breaking Changes * `verify(byte[] password, int cost, byte[] salt, byte[] rawBcryptHash23Bytes)` signature changed, added `version` property (see #24) +* `LongPasswordStrategies` factory methods now require the version for the max password length (see #22) +* Verifier now accepts `Version` as a constructor parameter and `verifyStrict` therefore does not need one (see #22) ## v0.8.0 diff --git a/README.md b/README.md index 9bb2308..a8ed14b 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ usually you should prefer `char[]` or `byte[]` APIs. If you want the hash verification to only verify for a specific version you can use `verifyStrict()` ```java -byte[] hash2y = BCrypt.with(BCrypt.Version.VERSION_2Y).hash(6, password.getBytes(StandardCharsets.UTF_8)); -BCrypt.Result resultStrict = BCrypt.verifyer().verifyStrict(password.getBytes(StandardCharsets.UTF_8), hash2y, BCrypt.Version.VERSION_2A); +byte[] hash2y = BCrypt.with(BCrypt.Version.VERSION_2Y).hash(6, password.getBytes(StandardCharsets.UTF_8)); +BCrypt.Result resultStrict = BCrypt.verifyer(BCrypt.Version.VERSION_2A).verifyStrict(password.getBytes(StandardCharsets.UTF_8), hash2y); // resultStrict.verified == false ``` diff --git a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java index 998fe20..d0ad21c 100644 --- a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java +++ b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java @@ -26,11 +26,6 @@ public final class BCrypt { */ public static final int SALT_LENGTH = 16; - /** - * The max length of the password in bytes excluding lats null-terminator byte - */ - public static final int MAX_PW_LENGTH_BYTE = 71; - /** * Minimum allowed cost factor */ @@ -55,52 +50,68 @@ public final class BCrypt { */ static final byte SEPARATOR = 0x24; + /** + * The default version of none is given + */ + static final Version DEFAULT_VERSION = Version.VERSION_2A; + private BCrypt() { } /** * Create a new instance of bcrypt hash with default version {@link Version#VERSION_2A}. - * Will throw an exception if given password is longer than the max length support for bycrpt of {@link #MAX_PW_LENGTH_BYTE}. + * Will throw an exception if given password is longer than the max length support for bycrpt of {@link Version#allowedMaxPwLength}. * * @return new bcrypt hash instance */ public static Hasher withDefaults() { - return new Hasher(Version.VERSION_2A, new SecureRandom(), new LongPasswordStrategy.StrictMaxPasswordLengthStrategy(MAX_PW_LENGTH_BYTE)); + return new Hasher(DEFAULT_VERSION, new SecureRandom(), LongPasswordStrategies.strict(DEFAULT_VERSION)); } /** * Create a new instance of bcrypt hash with given {@link Version}. - * Will throw an exception if given password is longer than the max length support for bycrpt of {@link #MAX_PW_LENGTH_BYTE}. + * Will throw an exception if given password is longer than the max length support for bycrpt of {@link Version#allowedMaxPwLength}. * * @param version defines what version of bcrypt will be generated (mostly the version identifier changes) * @return new bcrypt hash instance */ public static Hasher with(Version version) { - return new Hasher(version, new SecureRandom(), new LongPasswordStrategy.StrictMaxPasswordLengthStrategy(MAX_PW_LENGTH_BYTE)); + return new Hasher(version, new SecureRandom(), LongPasswordStrategies.strict(DEFAULT_VERSION)); } /** * Create a new instance of bcrypt hash with default version {@link Version#VERSION_2A}. * The passed {@link SecureRandom} is used for generating the random salt. - * Will throw an exception if given password is longer than the max length support for bycrpt of {@link #MAX_PW_LENGTH_BYTE}. + * Will throw an exception if given password is longer than the max length support for bycrpt of {@link Version#allowedMaxPwLength}. * * @param secureRandom to use for random salt generation * @return new bcrypt hash instance */ public static Hasher with(SecureRandom secureRandom) { - return new Hasher(Version.VERSION_2A, secureRandom, new LongPasswordStrategy.StrictMaxPasswordLengthStrategy(MAX_PW_LENGTH_BYTE)); + return new Hasher(DEFAULT_VERSION, secureRandom, LongPasswordStrategies.strict(DEFAULT_VERSION)); } /** * Create a new instance of bcrypt hash with default version {@link Version#VERSION_2A}. * The passed {@link LongPasswordStrategy} will decide what to do when the password is longer than the supported - * {@link #MAX_PW_LENGTH_BYTE} + * {@link Version#allowedMaxPwLength} * * @param longPasswordStrategy decides what to do on pw that are too long * @return new bcrypt hash instance */ public static Hasher with(LongPasswordStrategy longPasswordStrategy) { - return new Hasher(Version.VERSION_2A, new SecureRandom(), longPasswordStrategy); + return new Hasher(DEFAULT_VERSION, new SecureRandom(), longPasswordStrategy); + } + + /** + * Create a new instance with custom version and long password strategy + * + * @param version defines what version of bcrypt will be generated (mostly the version identifier changes) + * @param longPasswordStrategy decides what to do on pw that are too long + * @return new bcrypt hash instance + */ + public static Hasher with(Version version, LongPasswordStrategy longPasswordStrategy) { + return new Hasher(version, new SecureRandom(), longPasswordStrategy); } /** @@ -116,12 +127,23 @@ public static Hasher with(Version version, SecureRandom secureRandom, LongPasswo } /** - * Creates a new instance of bcrypt verifier to verify a password against a given hash + * Creates a new instance of bcrypt verifier to verify a password against a given hash. + * Uses {@link Version#VERSION_2A} and a strict long passwords strategy. * * @return new verifier instance */ public static Verifyer verifyer() { - return verifyer(LongPasswordStrategies.none()); + return verifyer(DEFAULT_VERSION); + } + + /** + * Creates a new instance of bcrypt verifier to verify a password against a given hash. + * + * @param version to use, also matters in {@link Verifyer#verifyStrict(byte[], byte[])} + * @return new verifier instance + */ + public static Verifyer verifyer(Version version) { + return new Verifyer(version, LongPasswordStrategies.strict(version)); } /** @@ -129,11 +151,12 @@ public static Verifyer verifyer() { * This verify also respects the passed {@link LongPasswordStrategy} for creating the reference hash - use this * if you use one while hashing. * + * @param version to use, also matters in {@link Verifyer#verifyStrict(byte[], byte[])} * @param longPasswordStrategy used to create the reference hash. * @return new verifier instance */ - public static Verifyer verifyer(LongPasswordStrategy longPasswordStrategy) { - return new Verifyer(longPasswordStrategy); + public static Verifyer verifyer(Version version, LongPasswordStrategy longPasswordStrategy) { + return new Verifyer(version, longPasswordStrategy); } /** @@ -281,7 +304,7 @@ public HashData hashRaw(int cost, byte[] salt, byte[] password) { 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)) { + if (password.length > version.allowedMaxPwLength + (version.appendNullTerminator ? 0 : 1)) { password = longPasswordStrategy.derive(password); } @@ -377,9 +400,11 @@ public String toString() { public static final class Verifyer { private final Charset defaultCharset = DEFAULT_CHARSET; private final LongPasswordStrategy longPasswordStrategy; + private final Version version; - private Verifyer(LongPasswordStrategy longPasswordStrategy) { - this.longPasswordStrategy = longPasswordStrategy; + private Verifyer(Version version, LongPasswordStrategy longPasswordStrategy) { + this.version = Objects.requireNonNull(version); + this.longPasswordStrategy = Objects.requireNonNull(longPasswordStrategy); } /** @@ -388,14 +413,15 @@ private Verifyer(LongPasswordStrategy longPasswordStrategy) { *

* If given hash has an invalid format {@link Result#validFormat} will be false; see also * {@link Result#formatErrorMessage} for easier debugging. + *

+ * Using the strict method will also require the version identifier to match, matching hash does not suffice. * - * @param password to compare against the hash - * @param bcryptHash to compare against the password - * @param expectedVersion will check for this version and wil not verify if versions do not match + * @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 verifyStrict(byte[] password, byte[] bcryptHash, Version expectedVersion) { - return verify(password, bcryptHash, expectedVersion); + public Result verifyStrict(byte[] password, byte[] bcryptHash) { + return innerVerify(password, bcryptHash, true); } /** @@ -409,7 +435,7 @@ public Result verifyStrict(byte[] password, byte[] bcryptHash, Version expectedV * @return result object, see {@link Result} for more info */ public Result verify(byte[] password, byte[] bcryptHash) { - return verify(password, bcryptHash, null); + return innerVerify(password, bcryptHash, false); } /** @@ -418,14 +444,15 @@ public Result verify(byte[] password, byte[] bcryptHash) { *

* If given hash has an invalid format {@link Result#validFormat} will be false; see also * {@link Result#formatErrorMessage} for easier debugging. + *

+ * Using the strict method will also require the version identifier to match, matching hash does not suffice. * - * @param password to compare against the hash - * @param bcryptHash to compare against the password - * @param expectedVersion will check for this version and wil not verify if versions do not match + * @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 verifyStrict(char[] password, char[] bcryptHash, Version expectedVersion) { - return verify(password, bcryptHash, expectedVersion); + public Result verifyStrict(char[] password, char[] bcryptHash) { + return innerVerify(password, bcryptHash, true); } /** @@ -439,7 +466,7 @@ public Result verifyStrict(char[] password, char[] bcryptHash, Version expectedV * @return result object, see {@link Result} for more info */ public Result verify(char[] password, char[] bcryptHash) { - return verify(password, bcryptHash, null); + return innerVerify(password, bcryptHash, false); } /** @@ -455,7 +482,7 @@ public Result verify(char[] password, char[] bcryptHash) { * @return result object, see {@link Result} for more info */ public Result verify(char[] password, CharSequence bcryptHash) { - return verify(password, toCharArray(bcryptHash), null); + return innerVerify(password, toCharArray(bcryptHash), false); } /** @@ -474,7 +501,7 @@ public Result verify(char[] password, CharSequence bcryptHash) { */ public Result verify(char[] password, byte[] bcryptHash) { try (MutableBytes pw = Bytes.from(password, defaultCharset).mutable()) { - return verify(pw.array(), bcryptHash, null); + return innerVerify(pw.array(), bcryptHash, false); } } @@ -490,13 +517,13 @@ private static char[] toCharArray(CharSequence charSequence) { } } - private Result verify(char[] password, char[] bcryptHash, Version requiredVersion) { + private Result innerVerify(char[] password, char[] bcryptHash, boolean strict) { byte[] passwordBytes = null; byte[] bcryptHashBytes = null; try { passwordBytes = Bytes.from(password, defaultCharset).array(); bcryptHashBytes = Bytes.from(bcryptHash, defaultCharset).array(); - return verify(passwordBytes, bcryptHashBytes, requiredVersion); + return innerVerify(passwordBytes, bcryptHashBytes, strict); } finally { Bytes.wrapNullSafe(passwordBytes).mutable().secureWipe(); Bytes.wrapNullSafe(bcryptHashBytes).mutable().secureWipe(); @@ -506,18 +533,17 @@ private Result verify(char[] password, char[] bcryptHash, Version requiredVersio /** * Verify given password against a bcryptHash */ - private Result verify(byte[] password, byte[] bcryptHash, Version requiredVersion) { + private Result innerVerify(byte[] password, byte[] bcryptHash, boolean strict) { Objects.requireNonNull(bcryptHash); - BCryptParser parser = requiredVersion == null ? Version.VERSION_2A.parser : requiredVersion.parser; try { - HashData hashData = parser.parse(bcryptHash); + HashData hashData = this.version.parser.parse(bcryptHash); - if (requiredVersion != null && hashData.version != requiredVersion) { + if (strict && hashData.version != this.version) { return new Result(hashData, false); } - return verify(password, hashData.cost, hashData.rawSalt, hashData.rawHash, hashData.version); + return verify(password, hashData.cost, hashData.rawSalt, hashData.rawHash); } catch (IllegalBCryptFormatException e) { return new Result(e); } @@ -538,7 +564,7 @@ private Result verify(byte[] password, byte[] bcryptHash, Version requiredVersio * @return result object, see {@link Result} for more info */ public Result verify(byte[] password, HashData bcryptHashData) { - return verify(password, bcryptHashData.cost, bcryptHashData.rawSalt, bcryptHashData.rawHash, bcryptHashData.version); + return verify(password, bcryptHashData.cost, bcryptHashData.rawSalt, bcryptHashData.rawHash); } /** @@ -555,15 +581,14 @@ public Result verify(byte[] password, HashData bcryptHashData) { * @param cost cost (log2 factor) which was used to create the hash * @param salt 16 byte raw hash value (not radix64 version) which was used to create the hash * @param rawBcryptHash23Bytes 23 byte raw bcrypt hash value (not radix64 version) - * @param version the version of the provided hash * @return result object, see {@link Result} for more info */ - public Result verify(byte[] password, int cost, byte[] salt, byte[] rawBcryptHash23Bytes, Version version) { + public Result verify(byte[] password, int cost, byte[] salt, byte[] rawBcryptHash23Bytes) { Objects.requireNonNull(password); Objects.requireNonNull(rawBcryptHash23Bytes); Objects.requireNonNull(salt); - HashData hashData = BCrypt.with(version, new SecureRandom(), longPasswordStrategy).hashRaw(cost, salt, password); + HashData hashData = BCrypt.with(this.version, longPasswordStrategy).hashRaw(cost, salt, password); return new Result(hashData, Bytes.wrap(hashData.rawHash).equalsConstantTime(rawBcryptHash23Bytes)); } } @@ -643,6 +668,16 @@ public static final class Version { 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); + /** + * Absolutely maximum length bcrypt can support (18x32bit) + */ + public static final int MAX_PW_LENGTH_BYTE = 72; + + /** + * The max length of the password in bytes excluding lats null-terminator byte + */ + public static final int DEFAULT_MAX_PW_LENGTH_BYTE = MAX_PW_LENGTH_BYTE - 1; + /** * $2a$ *

@@ -681,11 +716,18 @@ public static final class Version { */ public static final Version VERSION_2Y = new Version(new byte[]{MAJOR_VERSION, 0x79}, DEFAULT_FORMATTER, DEFAULT_PARSER); + /** + * $2y$ (2011) without the null terminator + *

+ * See {@link #VERSION_2Y} + */ + public static final Version VERSION_2Y_NO_NULL_TERMINATOR = new Version(new byte[]{MAJOR_VERSION, 0x79}, true, false, MAX_PW_LENGTH_BYTE, 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); + public static final Version VERSION_BC = new Version(new byte[]{MAJOR_VERSION, 0x63}, false, false, DEFAULT_MAX_PW_LENGTH_BYTE, DEFAULT_FORMATTER, DEFAULT_PARSER); /** * List of supported versions @@ -709,6 +751,13 @@ public static final class Version { */ public final boolean appendNullTerminator; + /** + * The max allowed length of password in bcrypt, longer than that {@link LongPasswordStrategy} will be activated. + * Usual lengths are between 50 and 72 bytes, most often are 56, 71 or 72 bytes. + * See https://security.stackexchange.com/a/39851 + */ + public final int allowedMaxPwLength; + /** * The formatter for the bcrypt message digest */ @@ -720,7 +769,7 @@ public static final class Version { public final BCryptParser parser; private Version(byte[] versionIdentifier, BCryptFormatter formatter, BCryptParser parser) { - this(versionIdentifier, true, true, formatter, parser); + this(versionIdentifier, true, true, DEFAULT_MAX_PW_LENGTH_BYTE, formatter, parser); } /** @@ -730,14 +779,20 @@ private Version(byte[] versionIdentifier, BCryptFormatter formatter, BCryptParse * @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 allowedMaxPwLength the max allowed length of password in bcrypt, longer than that {@link LongPasswordStrategy} will be activated * @param formatter the formatter responsible for formatting the out hash message digest */ - public Version(byte[] versionIdentifier, boolean useOnly23bytesForHash, boolean appendNullTerminator, BCryptFormatter formatter, BCryptParser parser) { + public Version(byte[] versionIdentifier, boolean useOnly23bytesForHash, boolean appendNullTerminator, int allowedMaxPwLength, BCryptFormatter formatter, BCryptParser parser) { this.versionIdentifier = versionIdentifier; this.useOnly23bytesForHash = useOnly23bytesForHash; this.appendNullTerminator = appendNullTerminator; + this.allowedMaxPwLength = allowedMaxPwLength; this.formatter = formatter; this.parser = parser; + + if (allowedMaxPwLength > MAX_PW_LENGTH_BYTE) { + throw new IllegalArgumentException("allowed max pw length cannot be gt " + MAX_PW_LENGTH_BYTE); + } } @Override @@ -747,13 +802,13 @@ public boolean equals(Object o) { Version version = (Version) o; return useOnly23bytesForHash == version.useOnly23bytesForHash && appendNullTerminator == version.appendNullTerminator && + allowedMaxPwLength == version.allowedMaxPwLength && Arrays.equals(versionIdentifier, version.versionIdentifier); } @Override public int hashCode() { - - int result = Objects.hash(useOnly23bytesForHash, appendNullTerminator); + int result = Objects.hash(useOnly23bytesForHash, appendNullTerminator, allowedMaxPwLength); result = 31 * result + Arrays.hashCode(versionIdentifier); return result; } diff --git a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategies.java b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategies.java index 83b6931..75a0d63 100644 --- a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategies.java +++ b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategies.java @@ -11,28 +11,31 @@ private LongPasswordStrategies() { /** * See {@link at.favre.lib.crypto.bcrypt.LongPasswordStrategy.TruncateStrategy} * + * @param version required to get the max allowed pw length * @return new instance */ - public static LongPasswordStrategy truncate() { - return new LongPasswordStrategy.TruncateStrategy(BCrypt.MAX_PW_LENGTH_BYTE); + public static LongPasswordStrategy truncate(BCrypt.Version version) { + return new LongPasswordStrategy.TruncateStrategy(version.allowedMaxPwLength); } /** * See {@link at.favre.lib.crypto.bcrypt.LongPasswordStrategy.Sha512DerivationStrategy} * + * @param version required to get the max allowed pw length * @return new instance */ - public static LongPasswordStrategy hashSha512() { - return new LongPasswordStrategy.Sha512DerivationStrategy(BCrypt.MAX_PW_LENGTH_BYTE); + public static LongPasswordStrategy hashSha512(BCrypt.Version version) { + return new LongPasswordStrategy.Sha512DerivationStrategy(version.allowedMaxPwLength); } /** * See {@link at.favre.lib.crypto.bcrypt.LongPasswordStrategy.StrictMaxPasswordLengthStrategy} * + * @param version required to get the max allowed pw length * @return new instance */ - public static LongPasswordStrategy strict() { - return new LongPasswordStrategy.StrictMaxPasswordLengthStrategy(BCrypt.MAX_PW_LENGTH_BYTE); + public static LongPasswordStrategy strict(BCrypt.Version version) { + return new LongPasswordStrategy.StrictMaxPasswordLengthStrategy(version.allowedMaxPwLength); } /** diff --git a/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/BcryptTest.java b/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/BcryptTest.java index 31b7d8d..27aaa2c 100644 --- a/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/BcryptTest.java +++ b/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/BcryptTest.java @@ -23,7 +23,8 @@ public class BcryptTest { @Rule public RepeatRule repeatRule = new RepeatRule(); - public static final Charset UTF_8 = StandardCharsets.UTF_8; + static final Charset UTF_8 = StandardCharsets.UTF_8; + private static final BCrypt.Version DEFAULT_VERSION = BCrypt.Version.VERSION_2A; private BcryptTestEntry[] testEntries = new BcryptTestEntry[]{ // see: https://stackoverflow.com/a/12761326/774398 @@ -85,10 +86,10 @@ public void readmeExamples() { BCrypt.Result result = BCrypt.verifyer().verify(password.getBytes(StandardCharsets.UTF_8), bcryptHashBytes); //verify strict byte[] hash2y = BCrypt.with(BCrypt.Version.VERSION_2Y).hash(6, password.getBytes(StandardCharsets.UTF_8)); - BCrypt.Result resultStrict = BCrypt.verifyer().verifyStrict(password.getBytes(StandardCharsets.UTF_8), hash2y, BCrypt.Version.VERSION_2A); + BCrypt.Result resultStrict = BCrypt.verifyer(BCrypt.Version.VERSION_2A).verifyStrict(password.getBytes(StandardCharsets.UTF_8), hash2y); //overlong passwords - BCrypt.with(LongPasswordStrategies.truncate()).hash(6, new byte[100]); - BCrypt.with(LongPasswordStrategies.hashSha512()).hash(6, new byte[100]); + BCrypt.with(LongPasswordStrategies.truncate(BCrypt.Version.VERSION_2Y)).hash(6, new byte[100]); + BCrypt.with(LongPasswordStrategies.hashSha512(BCrypt.Version.VERSION_2Y)).hash(6, new byte[100]); //custom salt and secure random BCrypt.withDefaults().hash(6, Bytes.random(16).array(), password.getBytes(StandardCharsets.UTF_8)); BCrypt.with(new SecureRandom()).hash(6, password.getBytes(StandardCharsets.UTF_8)); @@ -119,12 +120,12 @@ public void testSecureRandom() throws Exception { @Test public void testLongPasswordStrategy() throws Exception { - checkHash(BCrypt.with(new LongPasswordStrategy.TruncateStrategy(BCrypt.MAX_PW_LENGTH_BYTE))); + checkHash(BCrypt.with(new LongPasswordStrategy.TruncateStrategy(DEFAULT_VERSION.allowedMaxPwLength))); } @Test public void testFullyCustom() throws Exception { - checkHash(BCrypt.with(BCrypt.Version.VERSION_2Y, new SecureRandom(), new LongPasswordStrategy.TruncateStrategy(BCrypt.MAX_PW_LENGTH_BYTE))); + checkHash(BCrypt.with(BCrypt.Version.VERSION_2Y, new LongPasswordStrategy.TruncateStrategy(BCrypt.Version.VERSION_2Y.allowedMaxPwLength))); } private void checkHash(BCrypt.Hasher bCrypt) throws Exception { @@ -206,28 +207,28 @@ public void createHashWithCharPwNull() { @Test(expected = IllegalArgumentException.class) public void createHashWithPwTooLong() { - BCrypt.withDefaults().hash(6, new byte[16], new byte[BCrypt.MAX_PW_LENGTH_BYTE + 1]); + BCrypt.withDefaults().hash(6, new byte[16], new byte[DEFAULT_VERSION.allowedMaxPwLength + 1]); } @Test(expected = IllegalArgumentException.class) public void createHashWithPwTooLong2() { - BCrypt.withDefaults().hash(6, new byte[16], new byte[BCrypt.MAX_PW_LENGTH_BYTE + 2]); + BCrypt.withDefaults().hash(6, new byte[16], new byte[DEFAULT_VERSION.allowedMaxPwLength + 2]); } @Test public void testLongPassword() { - byte[] pw = Bytes.random(BCrypt.MAX_PW_LENGTH_BYTE).array(); + byte[] pw = Bytes.random(DEFAULT_VERSION.allowedMaxPwLength).array(); byte[] bcryptHashBytes = BCrypt.withDefaults().hash(4, pw); assertTrue(BCrypt.verifyer().verify(pw, bcryptHashBytes).verified); } @Test public void testLongTruncatedPassword() { - byte[] pw = Bytes.random(BCrypt.MAX_PW_LENGTH_BYTE + 2).array(); + byte[] pw = Bytes.random(DEFAULT_VERSION.allowedMaxPwLength + 2).array(); byte[] salt = Bytes.random(16).array(); - byte[] bcryptHashBytes1a = BCrypt.with(LongPasswordStrategies.truncate()).hash(4, salt, pw); - byte[] bcryptHashBytes1b = BCrypt.with(LongPasswordStrategies.truncate()).hash(4, salt, Bytes.wrap(pw).resize(BCrypt.MAX_PW_LENGTH_BYTE + 1, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array()); - byte[] bcryptHashBytes2 = BCrypt.withDefaults().hash(4, salt, Bytes.wrap(pw).resize(BCrypt.MAX_PW_LENGTH_BYTE, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array()); + byte[] bcryptHashBytes1a = BCrypt.with(LongPasswordStrategies.truncate(DEFAULT_VERSION)).hash(4, salt, pw); + byte[] bcryptHashBytes1b = BCrypt.with(LongPasswordStrategies.truncate(DEFAULT_VERSION)).hash(4, salt, Bytes.wrap(pw).resize(DEFAULT_VERSION.allowedMaxPwLength + 1, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array()); + byte[] bcryptHashBytes2 = BCrypt.withDefaults().hash(4, salt, Bytes.wrap(pw).resize(DEFAULT_VERSION.allowedMaxPwLength, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array()); assertArrayEquals(bcryptHashBytes1a, bcryptHashBytes1b); assertArrayEquals(bcryptHashBytes1a, bcryptHashBytes2); @@ -240,7 +241,7 @@ public void testVariousPwLengthShouldBeDifferentHashes() { Set hashes = new HashSet<>(); for (int i = 0; i < 72; i++) { - BCrypt.HashData data = BCrypt.with(LongPasswordStrategies.truncate()).hashRaw(4, salt, pw.resize(i, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array()); + BCrypt.HashData data = BCrypt.with(LongPasswordStrategies.truncate(DEFAULT_VERSION)).hashRaw(4, salt, pw.resize(i, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array()); String hashHexString = Bytes.wrap(data.rawHash).encodeHex(); assertFalse("hash already in set for length " + i, hashes.contains(hashHexString)); hashes.add(hashHexString); @@ -249,10 +250,10 @@ public void testVariousPwLengthShouldBeDifferentHashes() { @Test public void testLongHashedPassword() { - byte[] pw = Bytes.random(BCrypt.MAX_PW_LENGTH_BYTE + 2).array(); + byte[] pw = Bytes.random(DEFAULT_VERSION.allowedMaxPwLength + 2).array(); byte[] salt = Bytes.random(16).array(); - byte[] bcryptHashBytes1 = BCrypt.with(LongPasswordStrategies.hashSha512()).hash(4, salt, pw); - byte[] bcryptHashBytes2 = BCrypt.with(LongPasswordStrategies.hashSha512()).hash(4, salt, Bytes.wrap(pw).resize(BCrypt.MAX_PW_LENGTH_BYTE + 1, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array()); + byte[] bcryptHashBytes1 = BCrypt.with(LongPasswordStrategies.hashSha512(DEFAULT_VERSION)).hash(4, salt, pw); + byte[] bcryptHashBytes2 = BCrypt.with(LongPasswordStrategies.hashSha512(DEFAULT_VERSION)).hash(4, salt, Bytes.wrap(pw).resize(DEFAULT_VERSION.allowedMaxPwLength + 1, BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX).array()); assertFalse(Bytes.wrap(bcryptHashBytes1).equals(bcryptHashBytes2)); } @@ -282,7 +283,7 @@ public void verifyRawByteArrays2() { byte[] pw = Bytes.random(24).encodeBase36().getBytes(); BCrypt.HashData hash = bCrypt.hashRaw(7, Bytes.random(16).array(), pw); - BCrypt.Result result = BCrypt.verifyer().verify(pw, hash.cost, hash.rawSalt, hash.rawHash, hash.version); + BCrypt.Result result = BCrypt.verifyer().verify(pw, hash.cost, hash.rawSalt, hash.rawHash); assertResult(result, true, BCrypt.Version.VERSION_2A, 7); } @@ -302,7 +303,7 @@ public void verifyIncorrectStrictVersion() { byte[] pw = "78PHasdhklöALÖö".getBytes(); byte[] hash = bCrypt.hash(5, Bytes.random(16).array(), pw); - BCrypt.Result result = BCrypt.verifyer().verifyStrict(pw, hash, BCrypt.Version.VERSION_2A); + BCrypt.Result result = BCrypt.verifyer(BCrypt.Version.VERSION_2A).verifyStrict(pw, hash); assertResult(result, false, BCrypt.Version.VERSION_2Y, 5); } @@ -312,7 +313,7 @@ public void verifyIncorrectStrictVersionChars() { String pw = "8PAsdjhlkjhkjla_ääas#d"; char[] hash = bCrypt.hashToChar(5, pw.toCharArray()); - BCrypt.Result result = BCrypt.verifyer().verifyStrict(pw.toCharArray(), hash, BCrypt.Version.VERSION_2A); + BCrypt.Result result = BCrypt.verifyer(BCrypt.Version.VERSION_2A).verifyStrict(pw.toCharArray(), hash); assertResult(result, false, BCrypt.Version.VERSION_2X, 5); } @@ -325,9 +326,9 @@ public void verifyCorrectNonDefaultVersion() { BCrypt.HashData hash1 = bCrypt.hashRaw(cost, Bytes.random(16).array(), Bytes.from(pw).array()); char[] hash2 = bCrypt.hashToChar(cost, pw.toCharArray()); - assertResult(BCrypt.verifyer().verify(pw.toCharArray(), hash2), true, version, cost); - assertResult(BCrypt.verifyer().verifyStrict(pw.toCharArray(), hash2, version), true, version, cost); - assertResult(BCrypt.verifyer().verify(Bytes.from(pw).array(), hash1), true, version, cost); + assertResult(BCrypt.verifyer(version).verify(pw.toCharArray(), hash2), true, version, cost); + assertResult(BCrypt.verifyer(version).verifyStrict(pw.toCharArray(), hash2), true, version, cost); + assertResult(BCrypt.verifyer(version).verify(Bytes.from(pw).array(), hash1), true, version, cost); } private void assertResult(BCrypt.Result result, boolean verified, BCrypt.Version version, int cost) { @@ -400,14 +401,15 @@ 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}, true, true, null, null)); - assertEquals(BCrypt.Version.VERSION_2Y, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x79}, true, true, null, null)); + assertEquals(BCrypt.Version.VERSION_2A, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x61}, true, true, BCrypt.Version.DEFAULT_MAX_PW_LENGTH_BYTE, null, null)); + assertEquals(BCrypt.Version.VERSION_2Y, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x79}, true, true, BCrypt.Version.DEFAULT_MAX_PW_LENGTH_BYTE, null, null)); + assertEquals(BCrypt.Version.VERSION_2Y_NO_NULL_TERMINATOR, new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x79}, true, false, BCrypt.Version.MAX_PW_LENGTH_BYTE, 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}, true, true, null, null).hashCode()); + assertEquals(BCrypt.Version.VERSION_2A.hashCode(), new BCrypt.Version(new byte[]{MAJOR_VERSION, 0x61}, true, true, BCrypt.Version.DEFAULT_MAX_PW_LENGTH_BYTE, 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()); @@ -416,12 +418,50 @@ public void testVersionPojoMethods() { @Test public void testVerifierWithLongPasswordStrategy() { - LongPasswordStrategy truncate = LongPasswordStrategies.truncate(); + LongPasswordStrategy truncate = LongPasswordStrategies.truncate(BCrypt.Version.VERSION_2A); byte[] pw = Bytes.random(200).array(); byte[] hash = BCrypt.with(truncate).hash(4, pw); - assertTrue(BCrypt.verifyer(truncate).verify(pw, hash).verified); - assertFalse(BCrypt.verifyer().verify(pw, hash).verified); + assertTrue(BCrypt.verifyer(BCrypt.Version.VERSION_2A, truncate).verify(pw, hash).verified); + assertFalse(BCrypt.verifyer(BCrypt.Version.VERSION_2A, LongPasswordStrategies.none()).verify(pw, hash).verified); + } + + @Test + public void testWithNullTerminatorWithinPw_shouldNotTerminate() { + byte[] pw1 = Bytes.from("secret").append(0x00).append("butthereismore").array(); + byte[] pw2 = Bytes.from("secret").array(); + + byte[] salt = Bytes.random(16).array(); + + String hash1 = Bytes.wrap(BCrypt.withDefaults().hash(4, salt, pw1)).toString(); + String hash2 = Bytes.wrap(BCrypt.withDefaults().hash(4, salt, pw2)).toString(); + + assertNotEquals(hash1, hash2); + System.out.println(hash1 + "\n" + hash2); + } + + @Test + public void testVersionWithNullTerminator() { + char[] pw = "myverlongpasswordthatisatleast72charslongandlongnothisisnotlongenoughyou".toCharArray(); + assertEquals(72, pw.length); + assertEquals(72, Bytes.from(pw).length()); + + byte[] salt = Bytes.random(16).array(); + + byte[] hash1 = BCrypt.with(BCrypt.Version.VERSION_2Y_NO_NULL_TERMINATOR, LongPasswordStrategies.truncate(BCrypt.Version.VERSION_2Y_NO_NULL_TERMINATOR)).hash(4, salt, Bytes.from(pw).array()); + byte[] hash2 = BCrypt.with(BCrypt.Version.VERSION_2Y, LongPasswordStrategies.truncate(BCrypt.Version.VERSION_2Y)).hash(4, salt, Bytes.from(pw).array()); + + assertNotEquals(Bytes.wrap(hash1).encodeUtf8(), Bytes.wrap(hash2).encodeUtf8()); + System.out.println(Bytes.wrap(hash1).encodeUtf8() + "\n" + Bytes.wrap(hash2).encodeUtf8()); + } + + @Test + public void testReferenceValuesWithoutNullTerminator() { + char[] pw = "myverlongpasswordthatisatleast72charslongandlongnothisisnotlongenoughyou".toCharArray(); + + assertTrue(BCrypt.verifyer(BCrypt.Version.VERSION_2Y_NO_NULL_TERMINATOR).verify(pw, "$2y$04$d4CIUbwyucxm87BQnDWyI.xHDm2vyIZfBDOzjASNkn/yB.6lzLwOG".toCharArray()).verified); + assertTrue(BCrypt.verifyer(BCrypt.Version.VERSION_2Y_NO_NULL_TERMINATOR).verify(pw, "$2y$04$w8S7HTjIfG.8RRVOhLZWtuH6eei2l7NZ/VhYUrDJndAjDmOqK6E0W".toCharArray()).verified); + assertTrue(BCrypt.verifyer(BCrypt.Version.VERSION_2Y, LongPasswordStrategies.truncate(BCrypt.Version.VERSION_2Y)).verify(pw, "$2y$04$w8S7HTjIfG.8RRVOhLZWtu//55gj0VTX7XdNkQmDuPw.qQXsnvtkG".toCharArray()).verified); } } diff --git a/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategyTest.java b/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategyTest.java index 9027d8b..10f61c4 100644 --- a/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategyTest.java +++ b/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategyTest.java @@ -9,17 +9,18 @@ public class LongPasswordStrategyTest { private final int maxLength = 72; + private static final BCrypt.Version DEFAULT_VERSION = BCrypt.Version.VERSION_2A; @Test public void testFactory() { - assertNotNull(LongPasswordStrategies.hashSha512().derive(Bytes.random(100).array())); - assertNotNull(LongPasswordStrategies.truncate().derive(Bytes.random(100).array())); + assertNotNull(LongPasswordStrategies.hashSha512(DEFAULT_VERSION).derive(Bytes.random(100).array())); + assertNotNull(LongPasswordStrategies.truncate(DEFAULT_VERSION).derive(Bytes.random(100).array())); assertNotNull(LongPasswordStrategies.none().derive(Bytes.random(100).array())); } @Test(expected = IllegalArgumentException.class) public void testFactoryForStrictShouldThrowException() { - LongPasswordStrategies.strict().derive(Bytes.random(100).array()); + LongPasswordStrategies.strict(DEFAULT_VERSION).derive(Bytes.random(100).array()); } @Test From 0f0dca19801bbd741272121e80430fcc61a9088d Mon Sep 17 00:00:00 2001 From: pfavre Date: Mon, 21 Oct 2019 23:41:35 +0200 Subject: [PATCH 2/3] Fix tests and small bug --- .../java/at/favre/lib/crypto/bcrypt/BCrypt.java | 15 +++++---------- .../lib/crypto/bcrypt/LongPasswordStrategies.java | 8 +++++--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java index d0ad21c..984563c 100644 --- a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java +++ b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java @@ -50,11 +50,6 @@ public final class BCrypt { */ static final byte SEPARATOR = 0x24; - /** - * The default version of none is given - */ - static final Version DEFAULT_VERSION = Version.VERSION_2A; - private BCrypt() { } @@ -65,7 +60,7 @@ private BCrypt() { * @return new bcrypt hash instance */ public static Hasher withDefaults() { - return new Hasher(DEFAULT_VERSION, new SecureRandom(), LongPasswordStrategies.strict(DEFAULT_VERSION)); + return new Hasher(Version.VERSION_2A, new SecureRandom(), LongPasswordStrategies.strict(Version.VERSION_2A)); } /** @@ -76,7 +71,7 @@ public static Hasher withDefaults() { * @return new bcrypt hash instance */ public static Hasher with(Version version) { - return new Hasher(version, new SecureRandom(), LongPasswordStrategies.strict(DEFAULT_VERSION)); + return new Hasher(version, new SecureRandom(), LongPasswordStrategies.strict(version)); } /** @@ -88,7 +83,7 @@ public static Hasher with(Version version) { * @return new bcrypt hash instance */ public static Hasher with(SecureRandom secureRandom) { - return new Hasher(DEFAULT_VERSION, secureRandom, LongPasswordStrategies.strict(DEFAULT_VERSION)); + return new Hasher(Version.VERSION_2A, secureRandom, LongPasswordStrategies.strict(Version.VERSION_2A)); } /** @@ -100,7 +95,7 @@ public static Hasher with(SecureRandom secureRandom) { * @return new bcrypt hash instance */ public static Hasher with(LongPasswordStrategy longPasswordStrategy) { - return new Hasher(DEFAULT_VERSION, new SecureRandom(), longPasswordStrategy); + return new Hasher(Version.VERSION_2A, new SecureRandom(), longPasswordStrategy); } /** @@ -133,7 +128,7 @@ public static Hasher with(Version version, SecureRandom secureRandom, LongPasswo * @return new verifier instance */ public static Verifyer verifyer() { - return verifyer(DEFAULT_VERSION); + return verifyer(Version.VERSION_2A); } /** diff --git a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategies.java b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategies.java index 75a0d63..b440fc1 100644 --- a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategies.java +++ b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/LongPasswordStrategies.java @@ -1,5 +1,7 @@ package at.favre.lib.crypto.bcrypt; +import java.util.Objects; + /** * Factory for default {@link LongPasswordStrategy} implementatins */ @@ -15,7 +17,7 @@ private LongPasswordStrategies() { * @return new instance */ public static LongPasswordStrategy truncate(BCrypt.Version version) { - return new LongPasswordStrategy.TruncateStrategy(version.allowedMaxPwLength); + return new LongPasswordStrategy.TruncateStrategy(Objects.requireNonNull(version).allowedMaxPwLength); } /** @@ -25,7 +27,7 @@ public static LongPasswordStrategy truncate(BCrypt.Version version) { * @return new instance */ public static LongPasswordStrategy hashSha512(BCrypt.Version version) { - return new LongPasswordStrategy.Sha512DerivationStrategy(version.allowedMaxPwLength); + return new LongPasswordStrategy.Sha512DerivationStrategy(Objects.requireNonNull(version).allowedMaxPwLength); } /** @@ -35,7 +37,7 @@ public static LongPasswordStrategy hashSha512(BCrypt.Version version) { * @return new instance */ public static LongPasswordStrategy strict(BCrypt.Version version) { - return new LongPasswordStrategy.StrictMaxPasswordLengthStrategy(version.allowedMaxPwLength); + return new LongPasswordStrategy.StrictMaxPasswordLengthStrategy(Objects.requireNonNull(version).allowedMaxPwLength); } /** From e8e0706a890eb04e08a8306427490a71e8ca4dbc Mon Sep 17 00:00:00 2001 From: Patrick Favre-Bulle Date: Mon, 28 Oct 2019 22:18:06 +0100 Subject: [PATCH 3/3] Refactor verifier to infer version if none is given --- .../at/favre/lib/crypto/bcrypt/BCrypt.java | 82 +++++++++++++------ .../favre/lib/crypto/bcrypt/BCryptParser.java | 2 +- .../favre/lib/crypto/bcrypt/BcryptTest.java | 19 +++++ 3 files changed, 79 insertions(+), 24 deletions(-) diff --git a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java index 984563c..d1b3206 100644 --- a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java +++ b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCrypt.java @@ -123,12 +123,11 @@ public static Hasher with(Version version, SecureRandom secureRandom, LongPasswo /** * Creates a new instance of bcrypt verifier to verify a password against a given hash. - * Uses {@link Version#VERSION_2A} and a strict long passwords strategy. * * @return new verifier instance */ public static Verifyer verifyer() { - return verifyer(Version.VERSION_2A); + return verifyer(null, null); } /** @@ -398,8 +397,8 @@ public static final class Verifyer { private final Version version; private Verifyer(Version version, LongPasswordStrategy longPasswordStrategy) { - this.version = Objects.requireNonNull(version); - this.longPasswordStrategy = Objects.requireNonNull(longPasswordStrategy); + this.version = version; + this.longPasswordStrategy = longPasswordStrategy; } /** @@ -416,7 +415,7 @@ private Verifyer(Version version, LongPasswordStrategy longPasswordStrategy) { * @return result object, see {@link Result} for more info */ public Result verifyStrict(byte[] password, byte[] bcryptHash) { - return innerVerify(password, bcryptHash, true); + return innerVerifyBytes(password, bcryptHash, true); } /** @@ -430,7 +429,7 @@ public Result verifyStrict(byte[] password, byte[] bcryptHash) { * @return result object, see {@link Result} for more info */ public Result verify(byte[] password, byte[] bcryptHash) { - return innerVerify(password, bcryptHash, false); + return innerVerifyBytes(password, bcryptHash, false); } /** @@ -447,7 +446,7 @@ public Result verify(byte[] password, byte[] bcryptHash) { * @return result object, see {@link Result} for more info */ public Result verifyStrict(char[] password, char[] bcryptHash) { - return innerVerify(password, bcryptHash, true); + return innerVerifyChar(password, bcryptHash, true); } /** @@ -461,7 +460,7 @@ public Result verifyStrict(char[] password, char[] bcryptHash) { * @return result object, see {@link Result} for more info */ public Result verify(char[] password, char[] bcryptHash) { - return innerVerify(password, bcryptHash, false); + return innerVerifyChar(password, bcryptHash, false); } /** @@ -477,7 +476,7 @@ public Result verify(char[] password, char[] bcryptHash) { * @return result object, see {@link Result} for more info */ public Result verify(char[] password, CharSequence bcryptHash) { - return innerVerify(password, toCharArray(bcryptHash), false); + return innerVerifyChar(password, toCharArray(bcryptHash), false); } /** @@ -496,10 +495,13 @@ public Result verify(char[] password, CharSequence bcryptHash) { */ public Result verify(char[] password, byte[] bcryptHash) { try (MutableBytes pw = Bytes.from(password, defaultCharset).mutable()) { - return innerVerify(pw.array(), bcryptHash, false); + return innerVerifyBytes(pw.array(), bcryptHash, false); } } + /** + * Convert a string type to char array in the most efficient manner. + */ private static char[] toCharArray(CharSequence charSequence) { if (charSequence instanceof String) { return charSequence.toString().toCharArray(); @@ -512,13 +514,16 @@ private static char[] toCharArray(CharSequence charSequence) { } } - private Result innerVerify(char[] password, char[] bcryptHash, boolean strict) { + /** + * Verify given password against a bcryptHash with char types + */ + private Result innerVerifyChar(char[] password, char[] bcryptHash, boolean strict) { byte[] passwordBytes = null; byte[] bcryptHashBytes = null; try { passwordBytes = Bytes.from(password, defaultCharset).array(); bcryptHashBytes = Bytes.from(bcryptHash, defaultCharset).array(); - return innerVerify(passwordBytes, bcryptHashBytes, strict); + return innerVerifyBytes(passwordBytes, bcryptHashBytes, strict); } finally { Bytes.wrapNullSafe(passwordBytes).mutable().secureWipe(); Bytes.wrapNullSafe(bcryptHashBytes).mutable().secureWipe(); @@ -526,24 +531,49 @@ private Result innerVerify(char[] password, char[] bcryptHash, boolean strict) { } /** - * Verify given password against a bcryptHash + * Verify given password against a bcryptHash with byte types */ - private Result innerVerify(byte[] password, byte[] bcryptHash, boolean strict) { + private Result innerVerifyBytes(byte[] password, byte[] bcryptHash, boolean strict) { Objects.requireNonNull(bcryptHash); try { - HashData hashData = this.version.parser.parse(bcryptHash); + final Version usedVersion; + final HashData hashData; + + if (this.version == null) { + hashData = Version.VERSION_2A.parser.parse(bcryptHash); + usedVersion = hashData.version; + } else { + usedVersion = this.version; + hashData = usedVersion.parser.parse(bcryptHash); + } - if (strict && hashData.version != this.version) { - return new Result(hashData, false); + if (strict) { + if (this.version == null) { + throw new IllegalArgumentException("Using strict requires to define a Version. " + + "Try 'BCrypt.verifier(Version.VERSION_2A)'."); + } + if (hashData.version != this.version) { + return new Result(hashData, false); + } } - return verify(password, hashData.cost, hashData.rawSalt, hashData.rawHash); + return verifyBCrypt(usedVersion, determinePasswordStrategy(usedVersion), password, hashData.cost, hashData.rawSalt, hashData.rawHash); } catch (IllegalBCryptFormatException e) { return new Result(e); } } + private LongPasswordStrategy determinePasswordStrategy(Version usedVersion) { + LongPasswordStrategy usedLongPasswordStrategy; + if (this.longPasswordStrategy == null) { + usedLongPasswordStrategy = LongPasswordStrategies.strict(usedVersion); + } else { + usedLongPasswordStrategy = this.longPasswordStrategy; + } + return usedLongPasswordStrategy; + } + /** * Verify given raw byte arrays of salt, 23 byte bcrypt hash and password. This is handy if the bcrypt messages are not packaged * in the default Modular Crypt Format (see also {@link Hasher#hashRaw(int, byte[], byte[])}. @@ -579,12 +609,18 @@ public Result verify(byte[] password, HashData bcryptHashData) { * @return result object, see {@link Result} for more info */ public Result verify(byte[] password, int cost, byte[] salt, byte[] rawBcryptHash23Bytes) { - Objects.requireNonNull(password); - Objects.requireNonNull(rawBcryptHash23Bytes); - Objects.requireNonNull(salt); + Version usedVersion = this.version == null ? Version.VERSION_2A : this.version; + return verifyBCrypt(usedVersion, determinePasswordStrategy(usedVersion), password, cost, salt, rawBcryptHash23Bytes); + } - HashData hashData = BCrypt.with(this.version, longPasswordStrategy).hashRaw(cost, salt, password); - return new Result(hashData, Bytes.wrap(hashData.rawHash).equalsConstantTime(rawBcryptHash23Bytes)); + /** + * Raw bcrypt verification + */ + private static Result verifyBCrypt(Version version, LongPasswordStrategy longPasswordStrategy, + byte[] password, int cost, byte[] salt, byte[] rawBcryptHash23Bytes) { + HashData hashData = BCrypt.with(Objects.requireNonNull(version), Objects.requireNonNull(longPasswordStrategy)) + .hashRaw(cost, Objects.requireNonNull(salt), Objects.requireNonNull(password)); + return new Result(hashData, Bytes.wrap(hashData.rawHash).equalsConstantTime(Objects.requireNonNull(rawBcryptHash23Bytes))); } } diff --git a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCryptParser.java b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCryptParser.java index b90f82b..d82fa4a 100644 --- a/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCryptParser.java +++ b/modules/bcrypt/src/main/java/at/favre/lib/crypto/bcrypt/BCryptParser.java @@ -90,7 +90,7 @@ public BCrypt.HashData parse(byte[] bcryptHash) throws IllegalBCryptFormatExcept int parsedCostFactor; try { - parsedCostFactor = Integer.valueOf(new String(costBytes, defaultCharset)); + parsedCostFactor = Integer.parseInt(new String(costBytes, defaultCharset)); } catch (NumberFormatException e) { throw new IllegalBCryptFormatException("cannot parse cost factor '" + new String(costBytes, defaultCharset) + "'"); } diff --git a/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/BcryptTest.java b/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/BcryptTest.java index 27aaa2c..1978daa 100644 --- a/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/BcryptTest.java +++ b/modules/bcrypt/src/test/java/at/favre/lib/crypto/bcrypt/BcryptTest.java @@ -464,4 +464,23 @@ public void testReferenceValuesWithoutNullTerminator() { assertTrue(BCrypt.verifyer(BCrypt.Version.VERSION_2Y_NO_NULL_TERMINATOR).verify(pw, "$2y$04$w8S7HTjIfG.8RRVOhLZWtuH6eei2l7NZ/VhYUrDJndAjDmOqK6E0W".toCharArray()).verified); assertTrue(BCrypt.verifyer(BCrypt.Version.VERSION_2Y, LongPasswordStrategies.truncate(BCrypt.Version.VERSION_2Y)).verify(pw, "$2y$04$w8S7HTjIfG.8RRVOhLZWtu//55gj0VTX7XdNkQmDuPw.qQXsnvtkG".toCharArray()).verified); } + + @Test + public void verifyInferVersion() { + verifyInferedVersion("<.S.2K(Zq'", "$2y$04$VYAclAMpaXY/oqAo9yUpkuWmoYywaPzyhu56HxXpVltnBIfmO9tgu", BCrypt.Version.VERSION_2Y); + verifyInferedVersion("<.S.2K(Zq'", "$2x$04$VYAclAMpaXY/oqAo9yUpkuWmoYywaPzyhu56HxXpVltnBIfmO9tgu", BCrypt.Version.VERSION_2X); + verifyInferedVersion("<.S.2K(Zq'", "$2a$04$VYAclAMpaXY/oqAo9yUpkuWmoYywaPzyhu56HxXpVltnBIfmO9tgu", BCrypt.Version.VERSION_2A); + verifyInferedVersion("<.S.2K(Zq'", "$2b$04$VYAclAMpaXY/oqAo9yUpkuWmoYywaPzyhu56HxXpVltnBIfmO9tgu", BCrypt.Version.VERSION_2B); + } + + private void verifyInferedVersion(String pw, String hash, BCrypt.Version expectedVersion) { + BCrypt.Result result = BCrypt.verifyer().verify(pw.toCharArray(), hash.toCharArray()); + assertTrue(result.verified); + assertEquals(expectedVersion, result.details.version); + } + + @Test(expected = IllegalArgumentException.class) + public void verifyStrictWithoutVersionShouldThrow() { + BCrypt.verifyer().verifyStrict("<.S.2K(Zq'".toCharArray(), "$2a$04$VYAclAMpaXY/oqAo9yUpkuWmoYywaPzyhu56HxXpVltnBIfmO9tgu".toCharArray()); + } }