Wednesday, July 18, 2012

Encrypted User Signup/Login using hash_hmac

So I've been working with CodeIgniter for awhile now and needed a secure user login system. I chose to develop my own since I wanted to learn as much about building a framework as possible. At one point during development I realized I needed a good user password system that didn't store passwords as a hashed string. So I went online and implemented this solution:

Ok, before we do anything, we need to add a "site_key" to our application/config/config.php file:


/*
|------------------------------------------------------------------------
| Site Key
|------------------------------------------------------------------------
|
| This is the global site key used for secure password generation.
|
*/
$config['site_key'] = 'long_random_alphanumeric_string';


The site_key will be used below. I generated mine through KeePass. Now the signup controller:


// SIGNUP for user account controller:
public function validate_signup()
{
     // Use the form validation library to validate user input
     $this->load->library('form_validation');
     
     $this->form_validation->set_rules('...');

     // Run the form validation
     if ($this->form_validation->run() == FALSE)
     {
          $this->index("Optional custom error message!");
     }
     else // if form validation passed
     {
          // Run the INSERT statement and return TRUE/FALSE
          if ($this->user_model->InsertSanitize($_POST['password']))
          {
               redirect('login');
          }
          else
          {
               $this->index("ERROR: signup failed!");
          }
     }
}

The signup controller handles taking new user information, validating it, and then sending the information to the user model to be INSERTED. Next let's look at the user model:

public function InsertSanitize($password)
{
     // Retrieve user input data through POST:
     $user_name = $this->input->post('user_name');
     // Or passed through the method (choose one or the other, not both):
     $password = $password;


     // Do additional validation here if needed:
     if (!isset($user_name) || $user_name == "")
     {
          // You can also use regex or functions like is_numeric()
          return FALSE;
     }

     return $this->Insert($list_of_safe_values, $password);
}



private function Insert($list_of_safe_values, $password)
{
     // Now it's time for the fun stuff!
     // First we need to use crypt() to hash the input password
     $hashed_pass = crypt($password);
     
     // Next we need to call a custom function below (scroll down):
     $enc_pass = $this->encrypt($password, $hashed_pass);

     // Create an array of user info to INSERT
     // Be sure to store the encrypted and hashed password
     $data = array(
          'list_of_safe_values' => $list_of_safe_values,
          'enc_pass' => $enc_pass,
          'hashed_pass' => $hashed_pass
     );

     // Run the INSERT
     if ($this->db->insert('user_table', $data))
     {
          // Optional: Return the last records ID
          return mysql_insert_id();
          // Or simply:
          return TRUE;
     }
     else // if INSERT failed
     {
          return FALSE;
     }
}


private function encrypt($password, $nonce)
{
     // Retrieve the site_key
     $site_key = $this->config->item('site_key');

     // Return the encrypted password using hash_hmac

     return hash_hmac('sha512', $password . $nonce, $site_key);
}


Be sure when updating your website or overwriting files that you keep the site key! If you forget to re-add it, existing users won't be able to login and new users won't have the site_key attached to their accounts.

Finally, I'll show you how to use this system when logging in:

public function validate_login()
{
     // Just like before, run form validation

     $this->load->library('form_validation');
     
     $this->form_validation->set_rules('...');



     if ($this->form_validation->run() == FALSE)
     {
          $this->index("Optional custom error message!");
     }
     else // if form validation passed
     {
          // Next, run validation in the user model:
          $data = $this->user_model->Validate($_POST['password']);
     }
}



public function Validate($password)
{
     // Retrieve user input data through POST:
     $user_name = $this->input->post('user_name');
     // Or passed through the method:
     $password = $password;


     // Do additional validation here if needed:
     if (!isset($user_name) || $user_name == "")
     {
          // You can also use regex or functions like is_numeric()
          return FALSE;
     }

     // Next we need to call a custom function below (scroll down):
     $enc_pass = $this->retrieve($user_name, $password);



     // With the encrypt password we can now validate our user login
     $sql = sprintf(self::constant . " WHERE user_name = '%s'
          AND password = '%s' LIMIT 1",
          mysql_real_escape_string($user_name),
          mysql_real_escape_string($enc_pass));


     // Run your queries however you normally do
     return $this->LoadFromDb($sql);
}



private function retrieve($user_name, $password)
{
     // Find user by user_name
     $data = $this->FindUser($user_name);

     // Get the hashed user password

     if (isset($data))
     {
          foreach ($data as $obj)
          {
               $hashed_pass = $obj->hashed_pass;
          }
          return $this->encrypt($password, $hashed_pass);
     }
     else
     {
          return FALSE;
     }
}

And that's it! But before I leave, let me explain quickly in case it's a little unclear.

First we created a site key, which is used to further enhance the encryption process.

Then we allowed a user to signup, validating form data and then data that's passed to the model. Within the Insert() function we took the submitted user password and hashed it, creating a randomly generated, alphanumeric string. This is what is used as a key for decrypting the encrypted password. Then we called the encrypt() function which takes the user password and the hashed key, along with the site key and encrypts them altogether. We returned this encrypted password and INSERTED it into the database along with the hashed key and any other user data (like username, email, etc.).

Now that we have a new user account with an encrypted password, we need to login. We validate the login form and data that's passed to the model. If everything looks good, we call the retrieve() function which loads the requested user information. We grab the supposed user's hashed key, along with the password they provided at login, and run it through the encryption process again.

From here we created a query using the provided user_name and processed encrypted password to see if any entries in the database are returned. If there is a record, that means that the username and password the login provided, matched and was successful. If it fails, that means either the username or password was wrong, or the user doesn't even exist.



Hopefully that all makes sense. Good luck!


Note: sorry if the code looks messed up, Blogger apparently sucks at pasting...also we're technically never decrypting anything. Just encrypting again and comparing.

No comments:

Post a Comment