0

Trying to get an authentication service up and running, however there is a rather significant problem in that if I store the salt in the database, then retrieve it and attempt to generate a hash with the submitted password from the user, it generates an incorrect hash even if the password is the same.

For example, if I use my code to post a request of

{
    "Email":"stackOverflow@gmail.com",
    "Password":"foo"
}

It is hashed, and produces the values of:

  • Hashed Password: ��-o]��+ztu��k�
  • Salt: [B@681d1bc7

If I then retrieve the salt and attempt to authenticate, I instead receive the hashed password of:

  • Hashed Password: r��7����^J!/QF

The salt is the same, [B@681d1bc7, so unless there's inconsistent data structures I believe that is fine.

Database is Oracle SQL, with ID being a Number, and Email, Password and Password_Salt being Varchars.


Code below:

AuthenticationController:

/*
 * TODO: Change me
 */
package entities;

import importsJSON.JSONObject;
import importsJSON.JSONParser;
import java.io.InputStream;
import java.sql.SQLException;

public class AuthenticationController {

//<editor-fold desc="Logic related to instantiation of an AC object">
/**
 * Logic related to instantiation of an AC object.
 * To instantiate an AC object, due to the method of the Singleton Pattern, 
 * AuthenticationController ac = new AuthenticationController();
 * will not work, as the constructor is private.
 * Instead calls are made as such:
 * AuthenticationController ac = AuthenticationController.getInstance();
 * 
 * This means that there will only ever be a single AuthenticationController, which
 * bears the benefit that concurrency and multiple instances are eliminated as possibilities, 
 * something arguably desirable for such functionality as login management.
 * It does need to be noted however, that this can cause a potential bottleneck and harms scalability
 * of a single server. While it is good we don't have to worry about multiple logins or 
 * "losing" a logged in user, it's also bad in the sense that we can't have multiple AC instances.
 * If 3 users require authentication simultaneously, they cannot have have their requests 
 * delegated to different AC instances to process them faster.
 * Such a system would ideally be better for scalability, but would require a great deal more work in order to ensure 
 * everything is safe and issues like concurrency are avoided.
 */
private static AuthenticationController instance = new AuthenticationController();

private AuthenticationController(){}

public static AuthenticationController getInstance(){
    return instance;
}
//</editor-fold>

public String getMessage(){
    return "HelloWorld";
}

public String authenticateUser(String rawData) throws SQLException, Exception{     
    //Variables for building a User to evaluate
    String email = "";
    PasswordManager passwordManager = new PasswordManager();
    JSONObject user = null;
    byte[] salt = null;
    int userID;

    String results ="";

    //Variables for request evaluation
    Boolean badInput = false;
    String hashedUserPassword = null;
    String storedPassword = null;
    UserModel userModel = new UserModel();


    //Convert the input to a JSON Object
    JSONParser parser = new JSONParser();
    user = (JSONObject)parser.parse(rawData);     

    //Get User Email and Password  
    try{
        email = user.get("Email").toString();
        passwordManager.setPtPassword(user.get("Password").toString()); 
    }
    catch(Exception ex){
        badInput = true;
    }

    //If the user has not provided bad data, eg null values
    if(!badInput){
        //Create SQL query to search database for a row with matching email
        //Note that we only need the ID, Password and Salt, anything else is unnecessary
        String parameterlessQuery = "Select ID, PASSWORD, PASSWORD_SALT From Users Where UPPER(EMAIL) = UPPER(?)";
        String[] params = {email};
        SQLQuery query = new SQLQuery(parameterlessQuery, params);   

        if((email!=null)&&(email.length()>0)){
            query = authenticateUser(query);
        }
        else{
            query.setErrorCode("22000");
        }

        //Retrieve variables from results list
        if(!query.getResults().isEmpty()){
            Object[] userToVerify = query.getResults().get(0);                

            //userModel.setID(userToVerify[0].toString());
            userModel.sethPassword(userToVerify[1].toString());
            userModel.setSalt(userToVerify[2].toString());

            //Use the salt and provided password to create hashed password
            String userPassword = passwordManager.getPtPassword();
            hashedUserPassword  = passwordManager.generateHash(userPassword, userModel.getSalt().getBytes());
            System.out.println("New hashed: "+ hashedUserPassword+"   Hashed: "+ userModel.gethPassword()+ "    Salt:"+userModel.getSalt());
        }
    }            

    //Finally we need to convert them 


    //Compare  hashed password from Database to newly hashed password
    return (hashedUserPassword+"   "+ userModel.gethPassword());


    //If match, user is logged in

    //If no match, user receieves a negative reply informing them of failure.

}

//Method for submitting a query to a database
/** 
 * May seem redundant, however this is necessary in order to get back the query 
 * which was passed in so properties of the query can be examined.
 * Initialises a database controller and passes the query to be executed. 
 * @param query
 * @return 
 */
public SQLQuery authenticateUser(SQLQuery query){        
    DatabaseController databaseController = new DatabaseController();
    return databaseController.submitQuery(query, 3);
}

AuthenticationService:

package services;

import entities.AuthenticationController;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import javax.ejb.Stateless;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/Authenticate")
@Stateless
public class AuthenticationService {    

@POST
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public String loginUser(InputStream data) throws Exception {        
    AuthenticationController authController = AuthenticationController.getInstance();

    StringBuilder builder = new StringBuilder();

    try{
        BufferedReader in = new BufferedReader(new InputStreamReader(data));
        String line = null;
        while((line = in.readLine())!= null){
            builder.append(line);
        }
    } catch (IOException ex) {

    }
    return authController.authenticateUser(builder.toString());      
}
}

DatabaseController:

package entities;

import java.sql.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Andrew
 * Class controlling access to databases using SQL queries.
 */

public class DatabaseController extends AbstractController{

//<editor-fold desc="Constructors and class variables">    
String targetDB;//The database it shall use to connect to
String username;//The username needed to connect to the target database
String password;//The password needed to connect to the target database

//By default, this method shall be used.
public DatabaseController(){
   this.targetDB = [redacted];
   this.username = [redacted];
   this.password = [redacted];
} 

//Should we have more than one database to manipulate.
public DatabaseController(String targetDB, String username, String password){
   this.targetDB = targetDB;
   this.username = username;
   this.password = password;
}
//</editor-fold>

//Formats and submits the query to the given database.
public SQLQuery submitQuery(SQLQuery query, int columns){        
    try {

        //Using the Oracle DB connection Driver            
        Class.forName("oracle.jdbc.driver.OracleDriver");
        //Open a connection to the target Database
        Connection connection = DriverManager.getConnection(targetDB, username, password);           

        //Build then Execute Query         
        //This may look a bit weird. Unfortunately it's the result of the nuances of the 
        //classes used, (PreparedStatement instead of Statement), however the benefit of 
        //doing so is protection from SQL injections.
        PreparedStatement statement = connection.prepareStatement(query.getParameterlessQuery());           

        if(query.getQueryParams().length!=0){
            String[] params = query.getQueryParams();

            for(int i=0; i< params.length; i++){
                statement.setString((i+1), params[i]);                   
            }
        }

        //query.setDebug(statement.toString());


        query.setResponse(statement.executeQuery());

        //If there is a result set expected, then save the results before they are lost on connection close
        while(query.getResponse().next()){                
            Object[] temp = new Object[columns];
            for (int i = 0; i < columns; i++){
                temp[i] = query.getResponse().getObject(i+1);
            }

            query.getResults().add(temp);
        }


        //Close Connection
        connection.close();
   } catch (ClassNotFoundException ex) {
        Logger.getLogger(DatabaseController.class.getName()).log(Level.SEVERE, null, ex);
        query.setDebug("driver missing");
   } catch (SQLException ex) {
        for (Throwable e : ex) {
            if (e instanceof SQLException) {                   
                query.queryFail(((SQLException)e).getSQLState(), ((SQLException)e).getMessage());

                System.out.println(query.getErrorCode() + "\n"+ query.getErrorMessage());
            }
        }
   }
    return query;
}

}

RegistrationService:

package services;

import entities.DatabaseController;
import entities.PasswordManager;
import entities.SOAPEmailValidator;
import entities.SQLQuery;
import importsJSON.JSONObject;
import importsJSON.JSONParser;
import importsJSON.ParseException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.Stateless;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

/**
 *
 * @author Andrew
 * A Rather simple class that controls the registration services associated with 
 * our Web Service.
 */
@Path("/Register")
@Stateless
public class RegistrationService {    

    //Register user service
    /**
     * The default method for the /Register context of web services, meaning it is accessed
     * by a URI of form host/API/Register. 
     * Unlike most of the other methods, this is a post method and requires a sample of JSON 
     * data providing an email and password from the user in a form similar to:
     * {
     *      "Email":"[emailValue]",
     *      "Password":"[passwordValue]"
     * }
     * 
     * Everything else is then auto generated as necessary, and used to insert the user in to 
     * the database.
     * 
     * @param data
     * @return 
     */
    @POST
    @Path("/")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response registerUser(InputStream data){
        StringBuilder builder = new StringBuilder();

        //Variables for the building of the user to be inserted
        String email = "";
        String ptPassword = "";
        JSONObject user = null;
        PasswordManager password = new PasswordManager();
        byte[] salt = null;

        try{
            //Read in the submitted JSON
            BufferedReader in = new BufferedReader(new InputStreamReader(data));
            String line = null;
            while ((line = in.readLine()) != null) {
                builder.append(line);
            }

            //Convert the JSON String submitted by client to a JSON Object
            JSONParser p = new JSONParser();
            user = (JSONObject)p.parse(builder.toString());

            //Extract relevant values from the JSON Object
            email = user.get("Email").toString();
            ptPassword = user.get("Password").toString();

            //Generate Salt
            salt = password.generateSalt();

            //Use Salt to hash the password
            password.setHashedPassword(password.generateHash(ptPassword, salt));

        } catch (IOException ex) {
            Logger.getLogger(RegistrationService.class.getName()).log(Level.SEVERE, null, ex);        
        } catch (ParseException ex) {
            Logger.getLogger(RegistrationService.class.getName()).log(Level.SEVERE, null, ex);
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(RegistrationService.class.getName()).log(Level.SEVERE, null, ex);
        } catch (Exception ex) {
            Logger.getLogger(RegistrationService.class.getName()).log(Level.SEVERE, null, ex);
        }


        //Attempt to create a query
        String parameterlessQuery = "Insert into users (ID, EMAIL, PASSWORD, PASSWORD_SALT) VALUES (SEQ_PERSON_ID.nextval,?,?,?)";
        String[] params = {email, password.getHashedPassword(), password.saltToString(salt)};
        System.out.println("New Hashed:             "+"Hashed: "+ password.getHashedPassword() + "    Salt: "+ password.saltToString(salt));
        SQLQuery query = new SQLQuery(parameterlessQuery, params);

        //Check to ensure that the data is not null
        if((email!=null&&ptPassword!=null)&&(email.length()>0&&(ptPassword.length()>0))){
            query = registerUser(query);
        }else
        {
            query.setErrorCode("22000");
        }

        //If the error code suggests that the user has been created successfully, return a 200 OK message
        //to client
        if (query.getErrorCode().matches("00000")||query.getErrorCode().matches("24000")){
            return Response.status(200).entity("User Created successfully.").build();
        }
        else{//If not, return an error message with the SQLState code thrown
            return Response.status(400).entity("Failed, Error Code: "+query.getErrorCode()).build();
        }
    }


    //Method for submitting a query to a database
    /** 
     * May seem redundant, however this is necessary in order to get back the query 
     * which was passed in so properties of the query can be examined.
     * Initialises a database controller and passes the query to be executed. 
     * @param query
     * @return 
     */
    public SQLQuery registerUser(SQLQuery query){        
        DatabaseController databaseController = new DatabaseController();
        return databaseController.submitQuery(query, 0);        
    }

}

And finally, PasswordManager:

package entities;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.xml.bind.DatatypeConverter;

/**
 *
 * @author Andrew
 * A class controlling the logic relating to the hashing of passwords.
 * 
 */
public class PasswordManager {
    String ptPassword;//Plain text 
    String hashedPassword;//Hashed
    byte[] salt;

    public PasswordManager(){
        this.ptPassword = null;
        this.hashedPassword = null;
        this.salt = null;
    }

    //Generates a salt for hashing a password
    /**
     * Uses SecureRandom and the SHA-1 Pseudo Random Number Generator to create a 
     * salt that is extremely difficult to predict.
     * Possibly a bit overkill for the security on a simple travel application, however
     * this method is good for scalability were we looking to up our game.
     * 
     * @return 
     * @throws java.security.NoSuchAlgorithmException
     */
    public byte[] generateSalt() throws NoSuchAlgorithmException{
        SecureRandom secureRandomGenerator = SecureRandom.getInstance("SHA1PRNG");
        byte[] randomBytes = new byte[128];
        secureRandomGenerator.nextBytes(randomBytes);

        return randomBytes;
    }

    //Uses the plaintext password and the salt to generate a hashed password to store in the database
    /** 
     * 
     * @param password
     * @param salt
     * @return
     * @throws Exception 
     */
    public String generateHash(String password, byte[] salt) throws Exception{
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        SecretKey key = keyFactory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 2000, 128));

        String encoded = DatatypeConverter.printBase64Binary(key.getEncoded());
        byte[] decoded = DatatypeConverter.parseBase64Binary(encoded);

        String output = new String(decoded, "UTF-8");

        return output;
    } 


    //Helper method for conversion of salt to a database storable object
    /**
     * 
     * @param salt
     * @return 
     */
    public String saltToString(byte[] salt){
        return salt.toString();
    }

    //Helper method for conversion of salt to a hashable object
    /**
     * 
     * @param salt
     * @return 
     */
    public byte[] saltToBytes(String salt){
        return salt.getBytes();
    }

    public String getPtPassword() {
        return ptPassword;
    }

    public void setPtPassword(String ptPassword) {
        this.ptPassword = ptPassword;
    }

    public String getHashedPassword() {
        return hashedPassword;
    }

    public void setHashedPassword(String hashedPassword) {
        this.hashedPassword = hashedPassword;
    }

    public byte[] getSalt() {
        return salt;
    }

    public void setSalt(byte[] salt) {
        this.salt = salt;
    }


}
Andrew
  • 169
  • 1
  • 2
  • 15
  • 3
    That salt `[B@681d1bc7` is not what you think it is. It looks like you saved the `toString()` of the array instead of the actual content of the array, also the presence of unicode replacement characters in your hash suggests you are using the wrong way of persisting the hash (or at least, you might want to considering using a different way to show it to us, eg hexed or base64-ed). See also http://stackoverflow.com/questions/6684665/java-byte-array-to-string-to-byte-array – Mark Rotteveel Dec 30 '16 at 15:33
  • @MarkRotteveel A command outputting the salt values verifies this. Thanks for the heads up, I'll post the solution when I get things working. As for the presence of unicode chars, I feel it's because the hash is converted from base64 encoding to a string in order to store it. If you have any recommendations on that I'm listening. – Andrew Dec 30 '16 at 16:24

0 Answers0