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..d72b2de --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-02-04 - Timing Attacks in Hash/Token Verification +**Vulnerability:** Use of `equalsIgnoreCase` or `equals` for comparing sensitive hashes and tokens (Passwords, session cookies, etc.) allows for timing attacks. +**Learning:** These side-channel vulnerabilities were widespread in the codebase's authentication and token management modules. +**Prevention:** Always use `java.security.MessageDigest.isEqual` for constant-time comparison of sensitive data. 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..41d52af 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 @@ -4,6 +4,8 @@ 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 { @@ -26,13 +28,13 @@ 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 decodedHashedPassword = new String(Base64.getUrlDecoder().decode(hashedPassword), StandardCharsets.UTF_8); + String decodedHashedSaltPassword = new String(Base64.getUrlDecoder().decode(hashedSaltPassword), StandardCharsets.UTF_8); String salt = decodedHashedSaltPassword.substring(0, 9); String hashSP = decodedHashedSaltPassword.substring(9); String hashP = decodedHashedPassword.substring(9); @@ -40,6 +42,6 @@ public static boolean verifyPassword(String hashedPassword, String hashedSaltPas //System.out.println("verifyPassword, saltS: " + salt + " " + salt.length() + " | hashedSaltPassword: " + hashSP + " " + hashSP.length()); String hp = SHA256.hash(hashP + salt); - return hashSP.equalsIgnoreCase(hp); + return MessageDigest.isEqual(hashSP.getBytes(StandardCharsets.UTF_8), hp.getBytes(StandardCharsets.UTF_8)); } } diff --git a/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/controller/token/TokenManager.java b/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/controller/token/TokenManager.java index 4739e09..55f7d47 100644 --- a/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/controller/token/TokenManager.java +++ b/JFramework/crypto/src/main/java/it/richkmeli/jframework/crypto/controller/token/TokenManager.java @@ -4,6 +4,8 @@ import it.richkmeli.jframework.crypto.exception.CryptoException; import it.richkmeli.jframework.util.RandomStringGenerator; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -29,7 +31,7 @@ public static boolean verifyNumericCompact(String numericCompactToken, String va String extendedToken = generate(extractedNumericSalt + value, false); // put extractedNumericSalt at first chars of extendedToken extendedToken = reduceToNumeric(extractedNumericSalt + extendedToken, numericCompactToken.length()); - return numericCompactToken.equalsIgnoreCase(extendedToken); + return MessageDigest.isEqual(numericCompactToken.getBytes(StandardCharsets.UTF_8), extendedToken.getBytes(StandardCharsets.UTF_8)); } public static String generate(String value) { @@ -109,7 +111,7 @@ public static boolean verifyTemporized(String token, String value, int minutesOf hashed = token; calculatedHash = SHA256.hash(value + (currentTimeMinutes * minutesOfValidity)); } - return hashed.equalsIgnoreCase(calculatedHash); + return MessageDigest.isEqual(hashed.getBytes(StandardCharsets.UTF_8), calculatedHash.getBytes(StandardCharsets.UTF_8)); } private static String reduceToNumeric(String extendedToken, int length) { diff --git a/JFramework/network/src/main/java/it/richkmeli/jframework/network/tcp/server/http/util/ServletManager.java b/JFramework/network/src/main/java/it/richkmeli/jframework/network/tcp/server/http/util/ServletManager.java index 7cad720..2b06bf7 100644 --- a/JFramework/network/src/main/java/it/richkmeli/jframework/network/tcp/server/http/util/ServletManager.java +++ b/JFramework/network/src/main/java/it/richkmeli/jframework/network/tcp/server/http/util/ServletManager.java @@ -9,6 +9,8 @@ import it.richkmeli.jframework.util.log.Logger; import org.json.JSONObject; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -128,12 +130,18 @@ public static void initSessionCookie(HttpServletRequest request, HttpServletResp //Logger.info("Cookie JFRAMEWORKSESSIONID: already present"); cookie = getCookie(request, JFSESSIONID); } else { - String id = request.getRemoteAddr() + String encryptionKey = "default_secret_salt"; + try { + encryptionKey = ResourceBundle.getBundle("configuration").getString("encryptionkey"); + } catch (Exception e) { + // encryptionkey not found, using default + } + String id = encryptionKey + "##" + request.getRemoteAddr() //+ "##" + request.getRemoteUser() + "##" + getCookie(request, "JSESSIONID").getValue() + "##" + request.getHeader("User-Agent"); //Logger.info("Cookie JFRAMEWORKSESSIONID: " + id); - String hashed = SHA256.hashToString(id.getBytes()); + String hashed = SHA256.hashToString(id.getBytes(StandardCharsets.UTF_8)); cookie = new Cookie(JFSESSIONID, hashed); //cookie.setMaxAge(JFSESSIONCOOKIE_MAXAGE); } @@ -152,15 +160,21 @@ public static Cookie generateSessionCookie(HttpServletRequest request) { } String userAgent = request.getHeader("User-Agent"); - String id = remoteAddr + "##" + jsessionid + "##" + userAgent; + String encryptionKey = "default_secret_salt"; + try { + encryptionKey = ResourceBundle.getBundle("configuration").getString("encryptionkey"); + } catch (Exception e) { + // encryptionkey not found, using default + } + String id = encryptionKey + "##" + remoteAddr + "##" + jsessionid + "##" + userAgent; //Logger.info("Cookie JFRAMEWORKSESSIONID: " + id); - String hashed = SHA256.hashToString(id.getBytes()); + String hashed = SHA256.hashToString(id.getBytes(StandardCharsets.UTF_8)); return new Cookie(JFSESSIONID, hashed); } public static void checkSessionCookie(HttpServletRequest request, HttpServletResponse response) throws JServletException { Cookie extractedJframeworkSessionID = getCookie(request, JFSESSIONID); - if (!isCookiePresent(request, JFSESSIONID) || extractedJframeworkSessionID.getValue().equalsIgnoreCase("")) { + if (!isCookiePresent(request, JFSESSIONID) || extractedJframeworkSessionID.getValue().equals("")) { String warning = "Cookie: " + JFSESSIONID + " is not present in HTTP cookies"; Logger.warning(warning); // invalidate httpsession @@ -168,7 +182,7 @@ public static void checkSessionCookie(HttpServletRequest request, HttpServletRes //response.reset(); throw new JServletException(new KoResponse(BaseStatusCode.JFRAMEWORK_SESSIONID_ERROR)); } - if (!extractedJframeworkSessionID.getValue().equalsIgnoreCase(generateSessionCookie(request).getValue())) { + if (!MessageDigest.isEqual(extractedJframeworkSessionID.getValue().getBytes(StandardCharsets.UTF_8), generateSessionCookie(request).getValue().getBytes(StandardCharsets.UTF_8))) { String error = "JFRAMEWORKSESSIONID mismatch, possible Session Hijacking Attack"; Logger.error(error); resetSession(request, response); 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..99aaf69 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 @@ -6,6 +6,7 @@ public class RandomStringGenerator { public static final String ALPHANUMERIC_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; public static final String NUMERIC_ALPHABET = "0123456789"; + private static final SecureRandom random = new SecureRandom(); public static String generateAlphanumericString(int length) { String alphabet = ALPHANUMERIC_ALPHABET; @@ -21,10 +22,9 @@ 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))); + result.append(alphabet.charAt(random.nextInt(alphabetLength))); } return result.toString(); @@ -42,11 +42,9 @@ 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)); + int randomLimitedInt = random.nextInt(rightLimit - leftLimit + 1) + leftLimit; buffer.append((char) randomLimitedInt); } return buffer.toString(); @@ -55,19 +53,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); + random.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); + random.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); + random.nextBytes(array); return new String(array, StandardCharsets.US_ASCII); }