From 1946e04659f475e25e55fedfb75b5cbd20e5c8c9 Mon Sep 17 00:00:00 2001 From: firaja Date: Wed, 15 Feb 2023 18:12:32 +0100 Subject: [PATCH] #99: md5 and pbkdf2 conversion to operation on bytes --- .../java/com/password4j/BcryptFunction.java | 8 +- .../password4j/CompressedPBKDF2Function.java | 14 +-- src/main/java/com/password4j/Hash.java | 46 ++++++++-- .../com/password4j/MessageDigestFunction.java | 38 ++++++-- .../java/com/password4j/PBKDF2Function.java | 92 ++++++++++++------- src/main/java/com/password4j/Utils.java | 15 ++- src/test/com/password4j/PasswordTest.java | 4 +- 7 files changed, 156 insertions(+), 61 deletions(-) diff --git a/src/main/java/com/password4j/BcryptFunction.java b/src/main/java/com/password4j/BcryptFunction.java index a6324a62..2a844f37 100644 --- a/src/main/java/com/password4j/BcryptFunction.java +++ b/src/main/java/com/password4j/BcryptFunction.java @@ -234,7 +234,7 @@ public static BcryptFunction getInstanceFromHash(String hashed) else { char minor = hashed.charAt(2); - if (!isValidMinor(minor) || hashed.charAt(3) != '$') + if (isNotValidMinor(minor) || hashed.charAt(3) != '$') throw new BadParametersException("Invalid salt revision"); int rounds = Integer.parseInt(hashed.substring(4, 6)); return getInstance(Bcrypt.valueOf(minor), rounds); @@ -430,9 +430,9 @@ protected static int streamToWordMinorX(byte[] data, int[] offp) return streamToWords(data, offp, signp)[1]; } - private static boolean isValidMinor(char minor) + private static boolean isNotValidMinor(char minor) { - return Bcrypt.valueOf(minor) != null; + return Bcrypt.valueOf(minor) == null; } private static void internalChecks(String salt) @@ -751,7 +751,7 @@ protected Hash hash(byte[] passwordb, String salt) else { minor = salt.charAt(2); - if (!isValidMinor(minor) || salt.charAt(3) != '$') + if (isNotValidMinor(minor) || salt.charAt(3) != '$') throw new BadParametersException("Invalid salt revision"); off = 4; } diff --git a/src/main/java/com/password4j/CompressedPBKDF2Function.java b/src/main/java/com/password4j/CompressedPBKDF2Function.java index f1b0673e..df12f29e 100644 --- a/src/main/java/com/password4j/CompressedPBKDF2Function.java +++ b/src/main/java/com/password4j/CompressedPBKDF2Function.java @@ -146,16 +146,13 @@ public static CompressedPBKDF2Function getInstanceFromHash(String hashed) throw new BadParametersException("`" + hashed + "` is not a valid hash"); } - protected static String[] getParts(String hashed) - { - return hashed.split(new StringBuilder(2).append('\\').append(DELIMITER).toString()); - } + @Override - protected String getHash(byte[] encodedKey, String salt) + protected String getHash(byte[] encodedKey, byte[] salt) { String params = Long.toString((((long) getIterations()) << 32) | (getLength() & 0xffffffffL)); - String salt64 = Utils.encodeBase64(Utils.fromCharSequenceToBytes(salt)); + String salt64 = Utils.encodeBase64(salt); String hash64 = super.getHash(encodedKey, salt); return "$" + algorithm.code() + "$" + params + "$" + salt64 + "$" + hash64; } @@ -177,6 +174,11 @@ public boolean check(CharSequence plainTextPassword, String hashed, String salt) return slowEquals(internalHas.getResult(), hashed); } + protected static String[] getParts(String hashed) + { + return hashed.split(new StringBuilder(2).append('\\').append(DELIMITER).toString()); + } + private String getSaltFromHash(String hashed) { String[] parts = getParts(hashed); diff --git a/src/main/java/com/password4j/Hash.java b/src/main/java/com/password4j/Hash.java index 6d75aabb..e54790f0 100644 --- a/src/main/java/com/password4j/Hash.java +++ b/src/main/java/com/password4j/Hash.java @@ -28,7 +28,7 @@ * is used to verify the plain password; in addition cryptographic * seasoning such as salt and pepper are stored in this object. *

- * An hash is the product of a one-way function that maps data of arbitrary size to + * A hash is the product of a one-way function that maps data of arbitrary size to * fixed-size values; it is called hashing function (HF). * This class represent hashes generated by cryptographic hash function (CHF), * where each function has the following properties: @@ -78,7 +78,7 @@ public class Hash * Represents the salt: random data that is used as an additional input * to a cryptographic hashing function. */ - private String salt; + private byte[] salt; /** * Represents the pepper: a secret added to the input password @@ -122,6 +122,29 @@ private Hash() * @since 0.1.0 */ public Hash(HashingFunction hashingFunction, String result, byte[] bytes, String salt) + { + this.hashingFunction = hashingFunction; + this.salt = Utils.fromCharSequenceToBytes(salt); + this.result = result; + this.bytes = bytes; + } + + /** + * 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. + * Notice that the format varies depending on the algorithm. + * @param bytes the hash without additional information. + * @param salt the salt used for the computation. + * @since 0.1.0 + */ + public Hash(HashingFunction hashingFunction, String result, byte[] bytes, byte[] salt) { this.hashingFunction = hashingFunction; this.salt = salt; @@ -167,10 +190,21 @@ public HashingFunction getHashingFunction() /** * Retrieves the salt used by the hashing function. * - * @return the salt. + * @return the salt as {@link String}. * @since 0.1.0 */ public String getSalt() + { + return Utils.fromBytesToString(salt); + } + + /** + * Retrieves the salt used by the hashing function. + * + * @return the salt as bytes array. + * @since 1.7.0 + */ + public byte[] getSaltBytes() { return salt; } @@ -200,7 +234,7 @@ void setPepper(CharSequence pepper) } /** - * Produces a human readable description of the {@link Hash}. + * Produces a human-readable description of the {@link Hash}. * * @return a readable version of this object * @since 0.1.0 @@ -243,7 +277,7 @@ private boolean hasSameValues(Hash otherHash) { return areEquals(this.result, otherHash.result) // && Arrays.equals(this.bytes, otherHash.bytes) // - && areEquals(this.salt, otherHash.salt) // + && Arrays.equals(this.salt, otherHash.salt) // && areEquals(this.pepper, otherHash.pepper) // && this.hashingFunction.equals(otherHash.hashingFunction); } @@ -264,6 +298,6 @@ else if (cs1 != null && cs2 != null) @Override public int hashCode() { - return Objects.hash(result, salt, pepper, hashingFunction); + return Objects.hash(result, Arrays.hashCode(salt), pepper, hashingFunction); } } diff --git a/src/main/java/com/password4j/MessageDigestFunction.java b/src/main/java/com/password4j/MessageDigestFunction.java index fcf1ff7f..cb682bba 100644 --- a/src/main/java/com/password4j/MessageDigestFunction.java +++ b/src/main/java/com/password4j/MessageDigestFunction.java @@ -96,23 +96,33 @@ protected static String toString(String algorithm, SaltOption saltOption) @Override public Hash hash(CharSequence plainTextPassword) { - return internalHash(plainTextPassword, null); + return hash(plainTextPassword, null); + } + + public Hash hash(byte[] plainTextPasswordAsBytes) + { + return hash(plainTextPasswordAsBytes, null); } @Override public Hash hash(CharSequence plainTextPassword, String salt) { - return internalHash(plainTextPassword, salt); + return internalHash(Utils.fromCharSequenceToBytes(plainTextPassword), Utils.fromCharSequenceToBytes(salt)); + } + + public Hash hash(byte[] plainTextPasswordAsBytes, byte[] saltAsBytes) + { + return internalHash(plainTextPasswordAsBytes, saltAsBytes); } - protected Hash internalHash(CharSequence plainTextPassword, String salt) + protected Hash internalHash(byte[] plainTextPassword, byte[] salt) { try { MessageDigest messageDigest = MessageDigest.getInstance(algorithm); - CharSequence finalCharSequence = concatenateSalt(plainTextPassword, salt); + byte[] finalCharSequence = concatenateSalt(plainTextPassword, salt); - byte[] result = messageDigest.digest(Utils.fromCharSequenceToBytes(finalCharSequence)); + byte[] result = messageDigest.digest(finalCharSequence); return new Hash(this, Utils.toHex(result), result, salt); } catch (NoSuchAlgorithmException nsae) @@ -124,17 +134,27 @@ protected Hash internalHash(CharSequence plainTextPassword, String salt) @Override public boolean check(CharSequence plainTextPassword, String hashed) { - Hash hash = internalHash(plainTextPassword, null); - return slowEquals(hash.getResult(), hashed); + return check(plainTextPassword, hashed, null); + } + + public boolean check(byte[] plainTextPasswordAsBytes, byte[] hashed) + { + return check(plainTextPasswordAsBytes, hashed, null); } @Override public boolean check(CharSequence plainTextPassword, String hashed, String salt) { - Hash hash = internalHash(plainTextPassword, salt); + Hash hash = internalHash(Utils.fromCharSequenceToBytes(plainTextPassword), Utils.fromCharSequenceToBytes(salt)); return slowEquals(hash.getResult(), hashed); } + public boolean check(byte[] plainTextPassword, byte[] hashed, byte[] salt) + { + Hash hash = internalHash(plainTextPassword, salt); + return slowEquals(hash.getBytes(), hashed); + } + /** * The salt option describes if the Salt is appended or prepended to * the plain text password. @@ -158,7 +178,7 @@ public String getAlgorithm() return algorithm; } - private CharSequence concatenateSalt(CharSequence plainTextPassword, CharSequence salt) + private byte[] concatenateSalt(byte[] plainTextPassword, byte[] salt) { if (saltOption == SaltOption.PREPEND) { diff --git a/src/main/java/com/password4j/PBKDF2Function.java b/src/main/java/com/password4j/PBKDF2Function.java index 5ab787e4..9b1457b1 100644 --- a/src/main/java/com/password4j/PBKDF2Function.java +++ b/src/main/java/com/password4j/PBKDF2Function.java @@ -23,6 +23,7 @@ import javax.crypto.spec.PBEKeySpec; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -113,44 +114,28 @@ public static PBKDF2Function getInstance(String algorithm, int iterations, int l } } - protected static SecretKey internalHash(CharSequence plainTextPassword, String salt, String algorithm, int iterations, - int length) throws NoSuchAlgorithmException, InvalidKeySpecException - { - if (salt == null) - { - throw new IllegalArgumentException("Salt cannot be null"); - } - return internalHash(Utils.fromCharSequenceToChars(plainTextPassword), Utils.fromCharSequenceToBytes(salt), algorithm, - iterations, length); - } - protected static SecretKey internalHash(char[] plain, byte[] salt, String algorithm, int iterations, int length) - throws NoSuchAlgorithmException, InvalidKeySpecException - { - SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM_PREFIX + algorithm); - PBEKeySpec spec = new PBEKeySpec(plain, salt, iterations, length); - return secretKeyFactory.generateSecret(spec); - } - protected static String getUID(String algorithm, int iterations, int length) + @Override + public Hash hash(CharSequence plainTextPassword) { - return algorithm + "|" + iterations + "|" + length; + byte[] salt = SaltGenerator.generate(); + return hash(Utils.fromCharSequenceToBytes(plainTextPassword), salt); } - protected static String toString(String algorithm, int iterations, int length) + public Hash hash(byte[] plainTextPasswordAsBytes) { - return "a=" + algorithm + ", i=" + iterations + ", l=" + length; + byte[] salt = SaltGenerator.generate(); + return hash(plainTextPasswordAsBytes, salt); } @Override - public Hash hash(CharSequence plainTextPassword) + public Hash hash(CharSequence plainTextPassword, String salt) { - byte[] salt = SaltGenerator.generate(); - return hash(plainTextPassword, Utils.fromBytesToString(salt)); + return hash(Utils.fromCharSequenceToBytes(plainTextPassword), Utils.fromCharSequenceToBytes(salt)); } - @Override - public Hash hash(CharSequence plainTextPassword, String salt) + public Hash hash(byte[] plainTextPassword, byte[] salt) { try { @@ -165,11 +150,40 @@ public Hash hash(CharSequence plainTextPassword, String salt) } catch (IllegalArgumentException | InvalidKeySpecException e) { - String message = "Invalid specification with salt=" + salt + ", #iterations=" + iterations + " and length=" + length; + String message = "Invalid specification with salt=" + Arrays.toString(salt) + ", #iterations=" + iterations + " and length=" + length; throw new BadParametersException(message, e); } } + protected static SecretKey internalHash(byte[] plainTextPassword, byte[] salt, String algorithm, int iterations, int length) throws NoSuchAlgorithmException, InvalidKeySpecException + { + if (salt == null) + { + throw new IllegalArgumentException("Salt cannot be null"); + } + return internalHash(Utils.fromBytesToChars(plainTextPassword), salt, algorithm, iterations, length); + } + + protected static SecretKey internalHash(char[] plain, byte[] salt, String algorithm, int iterations, int length) + throws NoSuchAlgorithmException, InvalidKeySpecException + { + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM_PREFIX + algorithm); + PBEKeySpec spec = new PBEKeySpec(plain, salt, iterations, length); + return secretKeyFactory.generateSecret(spec); + } + + protected static String getUID(String algorithm, int iterations, int length) + { + return algorithm + "|" + iterations + "|" + length; + } + + protected static String toString(String algorithm, int iterations, int length) + { + return "a=" + algorithm + ", i=" + iterations + ", l=" + length; + } + + + /** * Overridable PBKDF2 generator * @@ -177,11 +191,23 @@ public Hash hash(CharSequence plainTextPassword, String salt) * @param salt cryptographic salt * @return the PBKDF2 hash string */ - protected String getHash(byte[] encodedKey, String salt) + protected String getHash(byte[] encodedKey, byte[] salt) { return Utils.encodeBase64(encodedKey); } + @Override + public boolean check(CharSequence plainTextPassword, String hashed) + { + return check((byte[]) null, null); + } + + public boolean check(byte[] plainTextPasswordAsBytes, byte[] hashed) + { + throw new UnsupportedOperationException("This implementation requires an explicit salt."); + + } + @Override public boolean check(CharSequence plainTextPassword, String hashed, String salt) { @@ -189,14 +215,14 @@ public boolean check(CharSequence plainTextPassword, String hashed, String salt) return slowEquals(internalHash.getResult(), hashed); } - @Override - public boolean check(CharSequence plainTextPassword, String hashed) + public boolean check(byte[] plainTextPasswordAsBytes, byte[] hashed, byte[] salt) { - throw new UnsupportedOperationException( - "This implementation requires an explicit salt. Use check(CharSequence, String, String) method instead."); - + Hash internalHash = hash(plainTextPasswordAsBytes, salt); + return slowEquals(internalHash.getBytes(), hashed); } + + public String getAlgorithm() { return algorithmAsString; diff --git a/src/main/java/com/password4j/Utils.java b/src/main/java/com/password4j/Utils.java index 856b0af2..f7ee589d 100644 --- a/src/main/java/com/password4j/Utils.java +++ b/src/main/java/com/password4j/Utils.java @@ -36,7 +36,7 @@ class Utils { - static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1; private static final char[] HEX_ALPHABET = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; @@ -120,6 +120,11 @@ static char[] fromCharSequenceToChars(CharSequence charSequence) return result; } + static char[] fromBytesToChars(byte[] bytes) + { + return new String(bytes, DEFAULT_CHARSET).toCharArray(); + } + static CharSequence append(CharSequence cs1, CharSequence cs2) { if (cs1 == null || cs1.length() == 0) @@ -143,6 +148,14 @@ static CharSequence append(CharSequence cs1, CharSequence cs2) } + static byte[] append(byte[] byteArray1, byte[] byteArray2) + { + byte[] result = new byte[byteArray1.length + byteArray2.length]; + System.arraycopy(byteArray1, 0, result, 0, byteArray1.length); + System.arraycopy(byteArray2, 0, result, byteArray1.length, byteArray2.length); + return result; + } + static String toHex(byte[] bytes) { final int length = bytes.length; diff --git a/src/test/com/password4j/PasswordTest.java b/src/test/com/password4j/PasswordTest.java index ff266a75..a3977e51 100644 --- a/src/test/com/password4j/PasswordTest.java +++ b/src/test/com/password4j/PasswordTest.java @@ -391,7 +391,7 @@ public void testBad7() @Test(expected = BadParametersException.class) public void testBad8() { - Password.check(PASSWORD, new Hash(null, null, null, null)); + Password.check(PASSWORD, new Hash((HashingFunction) null, (String) null, (byte[]) null, (String) null)); } @Test @@ -568,7 +568,7 @@ public void testBad6SecureString() @Test(expected = BadParametersException.class) public void testBad8SecureString() { - Password.check(SECURE_PASSWORD, new Hash(null, null, null, null)); + Password.check(SECURE_PASSWORD, new Hash((HashingFunction) null, (String) null, (byte[]) null, (String) null)); } @Test