diff --git a/.gitignore b/.gitignore index 70e39ba..1e3cefb 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ log.txt secureData*.txt logfile.txt rmc_log.txt +logfileConf.txt diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..da26f65 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-01-30 - Unsalted Password Hash Pattern +**Vulnerability:** The `PasswordManager` and `AuthDatabaseJframeworkManager` use a default unsalted password hashing scheme where a hardcoded salt `000000000` is prepended to a SHA-256 hash. +**Learning:** This pattern was likely chosen to allow client-side hashing without a challenge-response protocol, but it leaves the database vulnerable to offline brute-force and rainbow table attacks. +**Prevention:** Always use slow, salted hashing algorithms like Argon2 or BCrypt. If client-side hashing is required, use a proper challenge-response protocol where the server provide a per-user salt. diff --git a/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/algorithm/SHA256.java b/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/algorithm/SHA256.java index f1f7103..d6552e2 100644 --- a/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/algorithm/SHA256.java +++ b/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/algorithm/SHA256.java @@ -3,6 +3,8 @@ import org.bouncycastle.crypto.digests.SHA256Digest; import it.richkmeli.jframework.util.TypeConverter; +import java.nio.charset.StandardCharsets; + public class SHA256 { public static byte[] hash(byte[] input) { SHA256Digest digest = new SHA256Digest(); @@ -15,7 +17,7 @@ public static byte[] hash(byte[] input) { // sha256: string to hex public static String hash(String input) { - return TypeConverter.bytesToHex(hash(input.getBytes())); + return TypeConverter.bytesToHex(hash(input.getBytes(StandardCharsets.UTF_8))); } public static String hashToString(byte[] input) { diff --git a/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/controller/PasswordManager.java b/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/controller/PasswordManager.java index 7d2b934..fe0c37e 100644 --- a/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/controller/PasswordManager.java +++ b/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/controller/PasswordManager.java @@ -3,12 +3,15 @@ import it.richkmeli.jframework.crypto.algorithm.SHA256; import it.richkmeli.jframework.util.RandomStringGenerator; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.util.Base64; public class PasswordManager { // salt is enabled only during login process, instead set it as false for saving passwords into DB + // SECURITY WARNING: Storing unsalted hashes in DB (saltEnabled=false) is insecure. + // It is recommended to always use a random salt and a slow hashing algorithm like BCrypt. public static String hashPassword(String password, boolean saltEnabled) { // salt generation /*Random r = new SecureRandom(); @@ -26,20 +29,29 @@ public static String hashPassword(String password, boolean saltEnabled) { //System.out.println("hashPassword, saltS: " + saltS + " " + saltS.length() + " | hashedPassword: " + hashedPassword + " " + hashedPassword.length()); String out = saltS + hashedPassword; - return Base64.getUrlEncoder().encodeToString(out.getBytes(Charset.defaultCharset())); + return Base64.getUrlEncoder().encodeToString(out.getBytes(StandardCharsets.UTF_8)); } // hashedPassword = db password, hashedSaltPassword = login password public static boolean verifyPassword(String hashedPassword, String hashedSaltPassword) { - String decodedHashedPassword = new String(Base64.getUrlDecoder().decode(hashedPassword)); - String decodedHashedSaltPassword = new String(Base64.getUrlDecoder().decode(hashedSaltPassword)); - String salt = decodedHashedSaltPassword.substring(0, 9); - String hashSP = decodedHashedSaltPassword.substring(9); - String hashP = decodedHashedPassword.substring(9); + String decodedHashedPassword = new String(Base64.getUrlDecoder().decode(hashedPassword), StandardCharsets.UTF_8); + String decodedHashedSaltPassword = new String(Base64.getUrlDecoder().decode(hashedSaltPassword), StandardCharsets.UTF_8); + + // Fixed salt length of 9 characters + int saltLength = 9; + if (decodedHashedSaltPassword.length() < saltLength || decodedHashedPassword.length() < saltLength) { + return false; + } + + String salt = decodedHashedSaltPassword.substring(0, saltLength); + String hashSP = decodedHashedSaltPassword.substring(saltLength); + String hashP = decodedHashedPassword.substring(saltLength); //System.out.println("verifyPassword, saltS: " + salt + " " + salt.length() + " | hashedSaltPassword: " + hashSP + " " + hashSP.length()); String hp = SHA256.hash(hashP + salt); - return hashSP.equalsIgnoreCase(hp); + // Constant-time comparison to prevent timing attacks + return MessageDigest.isEqual(hashSP.toLowerCase().getBytes(StandardCharsets.UTF_8), + hp.toLowerCase().getBytes(StandardCharsets.UTF_8)); } } diff --git a/JFramework/util/src/main/java/it/richkmeli/jframework/util/RandomStringGenerator.java b/JFramework/util/src/main/java/it/richkmeli/jframework/util/RandomStringGenerator.java index a0a9007..3baa345 100644 --- a/JFramework/util/src/main/java/it/richkmeli/jframework/util/RandomStringGenerator.java +++ b/JFramework/util/src/main/java/it/richkmeli/jframework/util/RandomStringGenerator.java @@ -4,6 +4,7 @@ import java.security.SecureRandom; public class RandomStringGenerator { + private static final SecureRandom secureRandom = new SecureRandom(); public static final String ALPHANUMERIC_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; public static final String NUMERIC_ALPHABET = "0123456789"; @@ -21,7 +22,6 @@ private static String generateString(int length, String alphabet) { int alphabetLength = alphabet.length(); StringBuilder result = new StringBuilder(); - SecureRandom secureRandom = new SecureRandom(); for (int i = 0; i < length; ++i) { result.append(alphabet.charAt(secureRandom.nextInt(alphabetLength))); @@ -42,11 +42,10 @@ public static String generateBoundedString(int targetStringLength, int leftLimit //int leftLimit = 97; // letter 'a' //int rightLimit = 122; // letter 'z' //int targetStringLength = 10; - SecureRandom random = new SecureRandom(); StringBuilder buffer = new StringBuilder(targetStringLength); for (int i = 0; i < targetStringLength; i++) { int randomLimitedInt = leftLimit + (int) - (random.nextFloat() * (rightLimit - leftLimit + 1)); + (secureRandom.nextFloat() * (rightLimit - leftLimit + 1)); buffer.append((char) randomLimitedInt); } return buffer.toString(); @@ -55,19 +54,19 @@ public static String generateBoundedString(int targetStringLength, int leftLimit public static String generateUtf8String(int length) { byte[] array = new byte[length]; // length is bounded by 7 - new SecureRandom().nextBytes(array); + secureRandom.nextBytes(array); return new String(array, StandardCharsets.UTF_8); } public static String generateUtf16String(int length) { byte[] array = new byte[length]; // length is bounded by 7 - new SecureRandom().nextBytes(array); + secureRandom.nextBytes(array); return new String(array, StandardCharsets.UTF_16); } public static String generateASCIItring(int length) { byte[] array = new byte[length]; // length is bounded by 7 - new SecureRandom().nextBytes(array); + secureRandom.nextBytes(array); return new String(array, StandardCharsets.US_ASCII); }