Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ log.txt
secureData*.txt
logfile.txt
rmc_log.txt
logfileConf.txt
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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)));
Expand All @@ -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();
Expand All @@ -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);
}

Expand Down