1
\$\begingroup\$

here is my first virsion question: Is it good practice to have all my back end in one single file with all functions being static?

import java.sql.Statement;
import java.sql.Timestamp;



import java.sql.PreparedStatement;
import java.io.UnsupportedEncodingException;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import java.security.spec.InvalidKeySpecException;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;



//data base consists of columns [ID, username, password, numOfPasswordChanges, passwordChangeTimer]
public class DataBaseController {
    private static final int MAX_USERNAME_LENGTH = 20;
    private static final int MIN_USERNAME_LENGTH = 5;

    private static final int MAX_PASSWORD_LENGTH = 30;
    private static final int MIN_PASSWORD_LENGTH = 7;

    public static final int CORRECT_PASSWORD_RETURN_VAL  = 1;
    public static final int INCORRECT_PASSWORD_RETURN_VAL = 2;
    public static final int MAX_PASSWORD_RESETS_REACHED_RETURN_VAL = 3;

    public static final int WEAK_PASSWORD_RETURN_VAL = 4;
    public static final int WEAK_USERNAME_RETURN_VAL = 5;
    public static final int USER_ALREADY_EXISTS_RETURN_VAL = 6;
    public static final int USER_NOT_EXISTS_RETURN_VAL = 7;    
    

    private static final int USERNAME_INDEX = 2;
    private static final int PASSWORD_INDEX = 3;
    private static final int PASSWORD_RESET_INDEX = 4;
    private static final int RESET_TIMER_INDEX = 5;

    private static final int MAX_PASSWORD_RESETS = 3;

    private static final long RESET_TIME_TO_ADD = 20000L;
    
    private static final String ROOT_NAME = "root";       
    private static final String DB_PASSWORD = "root";
    private static final String DB_NAME = "mydb";
    Connection con;

    public DataBaseController() throws SQLException, ClassNotFoundException {
            Class.forName("com.mysql.jdbc.Driver");
            this.con = DriverManager.getConnection("jdbc:mysql://localhost:3306/" + DB_NAME, ROOT_NAME,DB_PASSWORD);
    }

    public boolean authorizeUser(String username, String password) throws Exception {


            String hashedPassword = new String(passwordHash(password), "UTF-8");
            System.out.println(hashedPassword);
            String query = "SELECT * FROM users WHERE username=?";
            PreparedStatement stmt = this.con.prepareStatement(query);

            stmt.setString(1, username);
            ResultSet rs = stmt.executeQuery();
            
            boolean userExists = rs.next();
            boolean correctLogin = false;
            if(userExists) {
                correctLogin = rs.getString(USERNAME_INDEX).equals(username) && rs.getString(PASSWORD_INDEX).equals(hashedPassword); 
            }

            return correctLogin;

    }

    public int register(String username, String password) throws ClassNotFoundException, SQLException, NoSuchAlgorithmException, InvalidKeySpecException, UnsupportedEncodingException {
        if(!checkIfUsernameValid(username)) {
            return WEAK_USERNAME_RETURN_VAL;
        }
        
        if(!checkIfPasswordValid(password)) {
            return WEAK_PASSWORD_RETURN_VAL;
        }

        String hashedPassword = new String(passwordHash(password), "UTF-8");
        String query = "SELECT * FROM users WHERE username = ?";
        PreparedStatement stmt = this.con.prepareStatement(query);
        stmt.setString(1, username);
        ResultSet rs = stmt.executeQuery();
        boolean doesUserExists = rs.next();
        
        
        if(!doesUserExists) {
            Statement idCheck = con.createStatement();
            String numberOfUsersQuery = "SELECT COUNT(*) FROM users";
            ResultSet userIDRs = idCheck.executeQuery(numberOfUsersQuery);
            String addUserSql = "";

            userIDRs.next();
            int newID = 1 + userIDRs.getInt(1);

            addUserSql = "INSERT INTO users(ID, username, password) VALUES(?, ?, ?)";
            stmt = this.con.prepareStatement(addUserSql);
            stmt.setInt(1, newID);
            stmt.setString(2, username);
            stmt.setString(3, hashedPassword);
            stmt.executeUpdate();

        }
        if(doesUserExists) {
            return USER_ALREADY_EXISTS_RETURN_VAL;
        }
        return USER_NOT_EXISTS_RETURN_VAL;
    }

    public int resetPassword(String username, String oldPassword, String newPassword) throws ClassNotFoundException, SQLException, UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException {

        String checkUserQuery = "SELECT * FROM users WHERE username = ? AND password = ?";
        PreparedStatement stmt = this.con.prepareStatement(checkUserQuery);
        stmt.setString(1, username);
        String oldPasswordHashed = new String(passwordHash(oldPassword), "UTF-8");
        stmt.setString(2, oldPasswordHashed);
        ResultSet rs = stmt.executeQuery();
        if(!rs.next()) {
            return INCORRECT_PASSWORD_RETURN_VAL;
        }

        if(!checkIfPasswordValid(newPassword)) {
            return WEAK_PASSWORD_RETURN_VAL;
        }

        String newPasswordHashed = new String(passwordHash(newPassword), "UTF-8");
        int numOfChanges = rs.getInt(PASSWORD_RESET_INDEX);
        Timestamp usersResetTimer = rs.getTimestamp(RESET_TIMER_INDEX);
        System.out.println(usersResetTimer + " **** " + new Timestamp(System.currentTimeMillis()) + " **** " + usersResetTimer.after(new Timestamp(System.currentTimeMillis())));
        if(!usersResetTimer.after(new Timestamp(System.currentTimeMillis()))) {
            numOfChanges = 0;
        }
        if(numOfChanges >= MAX_PASSWORD_RESETS) {
            return MAX_PASSWORD_RESETS_REACHED_RETURN_VAL;
        }

        
        String query = "UPDATE users SET password = ? WHERE username = ?"; 
        try {
            stmt = this.con.prepareStatement(query);
            stmt.setString(1, newPasswordHashed);
            stmt.setString(2, username);
            stmt.executeUpdate();

            numOfChanges += 1;
            query = "UPDATE users SET numOfPasswordChanges = ? WHERE username = ?";
            stmt = this.con.prepareStatement(query);
            stmt.setInt(1, numOfChanges);
            stmt.setString(2, username);
            stmt.executeUpdate();

            Timestamp newUserResetTimer = new Timestamp(System.currentTimeMillis() + RESET_TIME_TO_ADD);
            System.out.println(newUserResetTimer);
            String timeWithoutMillis = newUserResetTimer.toString().split("\\.")[0];
            System.out.println(newUserResetTimer.toString());

            query = "UPDATE users SET passwordChangeTimer = ? WHERE username = ?";
            stmt = this.con.prepareStatement(query);
            stmt.setString(1, timeWithoutMillis);
            stmt.setString(2, username);
            stmt.executeUpdate();

            
        } catch (Exception e) {
            e.printStackTrace();
        }
        return CORRECT_PASSWORD_RETURN_VAL;

    }

    private static boolean checkIfUsernameValid(String username) {
        if( username == null || username.length() > MAX_USERNAME_LENGTH || username.length() < MIN_USERNAME_LENGTH)
        {
            return false;
        }

        for(int i = 0; i < username.length(); i++) {
            if(!Character.isLetter(username.charAt(i)) && !Character.isDigit(username.charAt(i))) {
                return false;
            }
        }

        return true;

    }

    private static boolean checkIfPasswordValid(String password) {
        if(password == null || password.length() > MAX_PASSWORD_LENGTH || password.length() < MIN_PASSWORD_LENGTH) {
            return false;
        }

        boolean symbolFlag = false;
        boolean digitFlag = false;
        boolean letterFlag = false;

        for(int i = 0; i < password.length(); i++) {
            if(!Character.isLetter(password.charAt(i)) && !Character.isDigit(password.charAt(i))) {
                symbolFlag = true;
            }

            if(Character.isLetter(password.charAt(i))) {
                letterFlag = true;
            }

            if(Character.isDigit(password.charAt(i))) {
                digitFlag = true;
            }

            if(symbolFlag && digitFlag && letterFlag) {
                return true;
            }
        }

        return false;
    }

    private static byte[] passwordHash(String password) throws NoSuchAlgorithmException, InvalidKeySpecException, UnsupportedEncodingException {
        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
        byte[] hash = messageDigest.digest(password.getBytes("UTF-8"));
        return hash;
        
    }

}

I already posted here about this project before, got feedback and want to know if the problems still persist. so I changed to prepared statement to avoid sql injecntion, I also cached my DB connection in the constructor, and finally I hashed my passwords and then sent them to the data base. did I implement it correctly? I also tried salting my passwords but I couldnt figure out how, for some reason whenever I remove the salt from the hashed password it doesnt keep its structure (I hashed a password, added salt to it and when I hash the same password and remove the salt it is not the same if I remove the salt from the original) so for now only SHA-256.

what my code tries to achieve
I tried building a backed with a connection to a Database in my case I used MySQL.
so far I implemented authorizationg, regitration and password resets.
and addition to them I recently added a password hashing function but I wasnt able to add salt to it.

what my code does
first of all I have a bunch of files that are simple GUI for example the login and password page, and this file is where my backend. I have a constructor that creates a connection to my database once and each file that needs access to the backend creates an instance of it.
the columns in MySQL table are as follows in the same order: ID (unique key), username, password, numOfPasswordChanges, passwordChangeTimer

authorizeUser
first function is authorizeUser which take in a username and a password I use PreparedStatement to query my database so that it'll be more secure against SQL injection.

this is how I do the query to check for existing user.

String hashedPassword = new String(passwordHash(password), "UTF-8");

            String query = "SELECT * FROM users WHERE username=?";
            PreparedStatement stmt = this.con.prepareStatement(query);

            stmt.setString(1, username);
            ResultSet rs = stmt.executeQuery();

here I check if the password is correct

boolean userExists = rs.next();
            boolean correctLogin = false;
            if(userExists) {
                correctLogin = rs.getString(USERNAME_INDEX).equals(username) && rs.getString(PASSWORD_INDEX).equals(hashedPassword); 
            }

register
first of all it checks if given username and password are strong enough (for password I plan on using regex but still havent got around to it), then hashes the password that the user gives.

        String hashedPassword = new String(passwordHash(password), "UTF-8");
        String query = "SELECT * FROM users WHERE username = ?";
        PreparedStatement stmt = this.con.prepareStatement(query);
        stmt.setString(1, username);
        ResultSet rs = stmt.executeQuery();
        boolean doesUserExists = rs.next();

if user not exists it proceeds to add the new user to the DB

if(!doesUserExists) {
            Statement idCheck = con.createStatement();
            String numberOfUsersQuery = "SELECT COUNT(*) FROM users";
            ResultSet userIDRs = idCheck.executeQuery(numberOfUsersQuery);
            String addUserSql = "";

            userIDRs.next();
            int newID = 1 + userIDRs.getInt(1);

            addUserSql = "INSERT INTO users(ID, username, password) VALUES(?, ?, ?)";
            stmt = this.con.prepareStatement(addUserSql);
            stmt.setInt(1, newID);
            stmt.setString(2, username);
            stmt.setString(3, hashedPassword);
            stmt.executeUpdate();

        }

resetPassword
here I check for correct provided oldPassword with a PreparedStatement, then check if new password strong enough, int numOfChanges = rs.getInt(PASSWORD_RESET_INDEX); takes in the amount of tries the user tried to reset the password.

here I create a TimeStamp object to check if the timeout on resets has reached (currently 20 seconds after 3 resets).
then im setting the new password for the user.

Timestamp usersResetTimer = rs.getTimestamp(RESET_TIMER_INDEX);

        if(!usersResetTimer.after(new Timestamp(System.currentTimeMillis()))) {
            numOfChanges = 0;
        }
        if(numOfChanges >= MAX_PASSWORD_RESETS) {
            return MAX_PASSWORD_RESETS_REACHED_RETURN_VAL;
        }

here I update the amount of reset tries.

            numOfChanges += 1;
            query = "UPDATE users SET numOfPasswordChanges = ? WHERE username = ?";
            stmt = this.con.prepareStatement(query);
            stmt.setInt(1, numOfChanges);
            stmt.setString(2, username);
            stmt.executeUpdate();

and finally updating the timeout time in the DB

Timestamp newUserResetTimer = new Timestamp(System.currentTimeMillis() + RESET_TIME_TO_ADD);
            System.out.println(newUserResetTimer);
            String timeWithoutMillis = newUserResetTimer.toString().split("\\.")[0];
            System.out.println(newUserResetTimer.toString());

            query = "UPDATE users SET passwordChangeTimer = ? WHERE username = ?";
            stmt = this.con.prepareStatement(query);
            stmt.setString(1, timeWithoutMillis);
            stmt.setString(2, username);
            stmt.executeUpdate();

the return values of all of those functions are for the front end.

I also have this as my password hashing function

    private static byte[] passwordHash(String password) throws NoSuchAlgorithmException, InvalidKeySpecException, UnsupportedEncodingException {
        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
        byte[] hash = messageDigest.digest(password.getBytes("UTF-8"));
        return hash;
        
    }
\$\endgroup\$
3
  • 2
    \$\begingroup\$ To help reviewers give you better answers, we need to know what the code is intended to achieve. Please add sufficient context to your question to describe the purpose of the code. We want to know why much more than how. The more you tell us about what your code is for, the easier it will be for reviewers to help you. Also, edit the title to simply summarise the task, rather than your concerns about the code. \$\endgroup\$ Commented Jul 22, 2023 at 15:34
  • 2
    \$\begingroup\$ Also, you haven't shown the definition of your tables and indexes, without which it's hard to give a good review of database operations. I recommend you include these definitions (preferably as SQL statements, so that reviewers can reproduce your test environment). \$\endgroup\$ Commented Jul 22, 2023 at 15:35
  • \$\begingroup\$ @TobySpeight thanks for the feedback I edited my post and I hope it is more clear as to what my code is and what it does. \$\endgroup\$
    – Ellie
    Commented Jul 22, 2023 at 18:06

2 Answers 2

1
\$\begingroup\$

Code Reusability

  • You can write a method doesUserExists or findUserByUsername and enclose this functionality inside this method and reuse it in authorizeUser and register method.
String query = "SELECT * FROM users WHERE username = ?";
PreparedStatement stmt = this.con.prepareStatement(query);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
boolean doesUserExists = rs.next();

Flexiblity

  • Avoid hardcoding
  • For e.g. instead of passing "SHA-256" in hashed function. Take hashing algorithm as a method arguments. This way caller will have flexibility choosing type of hashing algorithm they want and it will be easy to switch to new implementation in future.
  • private static byte[] passwordHash(String password, String messageDigestAlgorithm)
  • Another option is to declare it in a constants.

Also avoid driver hardcoding

Class.forName("com.mysql.jdbc.Driver");
this.con = DriverManager.getConnection("jdbc:mysql://localhost:3306/" + DB_NAME, ROOT_NAME,DB_PASSWORD);

Unnecessary computation

  • Avoid unnecessary computation for e.g. in checkIfPasswordValid you're using if statements every time in for loop and that can be reduced to if-else if-else chain.

Constants

  • Some of your constants are private and some are public.
  • Follow one of the practice
    • Either declare all constants in dedicated Constants class
    • Or declare specific constants in related specific classes for e.g. database constants in database related class

Logging/Exception Handling

When you're validating username inside checkIfUsernameValid, Always add logging so that during failure we can get failure details and address the issue. Also you can create checked exception e.g. InvalidUsernameStringException with message username should be between x and y characters.

Also you can add null checks in the method for the method argument like this and be more fluent

Objects.requireNonNull(username, "username should be non null")
\$\endgroup\$
1
  • \$\begingroup\$ I did most of what you told me to, but in register and authorizeUser I still need both the PreparedStatement and the boolean value if the user exists or not, I was able to return the statement but I kept the boolean value outside of the new method and checked for existing user in both register and authorizeUser. \$\endgroup\$
    – Ellie
    Commented Jul 25, 2023 at 12:18
0
\$\begingroup\$
for(int i = 0; i < password.length(); i++) { 
    if(!Character.isLetter(password.charAt(i)) && !Character.isDigit(password.charAt(i))) {
        symbolFlag = true;
    }

    if(Character.isLetter(password.charAt(i))) {
         letterFlag = true;
    }

 
    if(Character.isDigit(password.charAt(i))) {
         digitFlag = true;
    }

    if(symbolFlag && digitFlag && letterFlag) {
         return true;
    }
}

In this section you perform the same tests multiple times, I think it might be possible to reduce it in a way that each test will be performed only once.

Good luck! :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.