From 10fce01093dd1b96fdec45b49860472a360578d7 Mon Sep 17 00:00:00 2001 From: firaja Date: Wed, 15 Feb 2023 18:43:31 +0100 Subject: [PATCH] #99: bcrypt conversion to operation on bytes --- .../java/com/password4j/BcryptFunction.java | 248 ++++++++++-------- src/main/java/com/password4j/Hash.java | 44 +++- .../com/password4j/BcryptFunctionTest.java | 17 +- 3 files changed, 181 insertions(+), 128 deletions(-) diff --git a/src/main/java/com/password4j/BcryptFunction.java b/src/main/java/com/password4j/BcryptFunction.java index 2a844f37..492e2303 100644 --- a/src/main/java/com/password4j/BcryptFunction.java +++ b/src/main/java/com/password4j/BcryptFunction.java @@ -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; @@ -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 @@ -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 @@ -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); } } diff --git a/src/main/java/com/password4j/Hash.java b/src/main/java/com/password4j/Hash.java index e54790f0..e60d4b24 100644 --- a/src/main/java/com/password4j/Hash.java +++ b/src/main/java/com/password4j/Hash.java @@ -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. @@ -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. @@ -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 pepper can be added with + * {@link #setPepper(CharSequence)}. + *

+ * 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; @@ -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; } @@ -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) // @@ -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); } } diff --git a/src/test/com/password4j/BcryptFunctionTest.java b/src/test/com/password4j/BcryptFunctionTest.java index 3933d51d..4b189cd1 100644 --- a/src/test/com/password4j/BcryptFunctionTest.java +++ b/src/test/com/password4j/BcryptFunctionTest.java @@ -20,6 +20,7 @@ import org.junit.Assert; import org.junit.Test; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.*; @@ -400,11 +401,11 @@ public void testInternationalChars() BcryptFunction function = BcryptFunction.getInstance(10); - String h1 = function.hash(pw1).getResult(); + String h1 = function.hash(pw1.getBytes(StandardCharsets.UTF_8)).getResult(); Assert.assertFalse(function.check(pw2, h1)); - String h2 = function.hash(pw2).getResult(); - Assert.assertFalse(function.check(pw1, h2)); + byte[] h2 = function.hash(pw2).getResultAsBytes(); + Assert.assertFalse(function.check(pw1.getBytes(StandardCharsets.UTF_8), h2)); } @@ -594,13 +595,13 @@ public void hashpwFailsWhenSaltIsTooShort() @Test public void equalsOnStringsIsCorrect() { - Assert.assertTrue(BcryptFunction.equalsNoEarlyReturn("", "")); - Assert.assertTrue(BcryptFunction.equalsNoEarlyReturn("test", "test")); + Assert.assertTrue(BcryptFunction.equalsNoEarlyReturn("".getBytes(), "".getBytes())); + Assert.assertTrue(BcryptFunction.equalsNoEarlyReturn("test".getBytes(), "test".getBytes())); - Assert.assertFalse(BcryptFunction.equalsNoEarlyReturn("test", "")); - Assert.assertFalse(BcryptFunction.equalsNoEarlyReturn("", "test")); + Assert.assertFalse(BcryptFunction.equalsNoEarlyReturn("test".getBytes(), "".getBytes())); + Assert.assertFalse(BcryptFunction.equalsNoEarlyReturn("".getBytes(), "test".getBytes())); - Assert.assertFalse(BcryptFunction.equalsNoEarlyReturn("test", "pass")); + Assert.assertFalse(BcryptFunction.equalsNoEarlyReturn("test".getBytes(), "pass".getBytes())); } @Test