Saturday, 23 April 2011

Secure log on form - Hashing passwords in JavaScript before submitting

I decided I'd like to encrypt user's passwords as they are logging in so that if they are on a network some malicious person running Wireshark, FireSheep etc can't read the user's actual password as it is being sent to the web server. I opted against using SSL for my basic site as I can't really justify the expense just to secure logins. I think you can get free SSL certificates now but even cheap web hosts charge to install them.

Initially I thought it could be done with public key cryptography using something like RSA:
  1. Public key is in the JavaScript external file, private key in PHP on the server
  2. User enters their username and password into the form and clicks Log In button
  3. The JavaScript runs and encrypts the password, storing it back in the text field
  4. Form is submitted to server and the password is decrypted with PHP
  5. Plain text password in PHP is salted & hashed then compared to salted hashed password in the database.
  6. Similar process for the registration/change password pages.
Ideally you'd need a nonce value as well so an attacker couldn't just re-send the encrypted value and gain access. I hunted around the net for a working JavaScript library to do it but none seem to be compatible with the PHP libraries available to handle the decryption. Unfortunately it would take a fair bit of time to try and rework some of the complex encryption routines to get them working together. So I posted on Stack Overflow and got an excellent response from Jeffrey Hantin about a method that could be used instead:
  1. User enters username and password and clicks Log In button
  2. JavaScript fires off an AJAX request to a PHP page which checks the username and outputs JSON containing the specific salt value for that user and a random nonce value (this helps prevent replay attacks)
  3. When the request completes (0-2 secs later) the JavaScript does hash( hash(salt + password) + nonce). The plus signs being string concatenation.
  4. The password+salt+nonce hash is stored back in a hidden field in the form and the form is submitted to the web server.
  5. The PHP page then gets the database's salted password hash and does hash ( databaseSaltedPasswordHash + nonce).
  6. This value is then compared to the hidden value passed through from the form.
  7. If it matches, the user is logged in, if not they are redirected back to the login page and number of attempts incremented.
Basically "hash must be a one-way function - given y and z such that z = hash(x, y), it must not be feasible to compute x. In order to compute the response value, the client has to know hash(password, salt) -- the server supplies salt so the client can derive it from password. The nonce is just to prevent reuse of a response."

I've coded it up in JavaScript and PHP and it works well. The plain text password is never sent in the clear to the server and someone intercepting the traffic can't resend the logon credentials to gain access. Apologies if these code snippets don't work exactly, I've cut them out of my main program and simplified them to give you the general idea.

Ok so here's the basic HTML code:

<head>
 <script type="text/javascript" src="/public/js/jquery.min.js"></script>
 <script type="text/javascript" src="/public/js/ajax.js"></script>
 <script type="text/javascript" src="/public/js/sha512.js"></script>
</head>

<body>
 <form id="loginForm" method="post" action="login_process.php">
 <table class="tableViewVertical">
  <tr>
   <th class="heading">Username</th>
   <td><input type="text" maxlength="50" name="username" id="username"></td>
  </tr>
  <tr>
   <th class="heading">Password</th>
   <td>
    <input type="hidden" name="hashedPassword" id="hashedPassword">
    <input type="password" maxlength="50" id="password">
   </td>
  </tr>
  <tr>
   <td colspan="2">
    <input type="button" value="Log in" id="btnHashPassword">
   </td>
  </tr>
  <tr>
   <td colspan="2"><span id="requestStatus"></span></td>
  </tr>
 </table>
 </form>
</body>

JavaScript on the page:

<script type="text/javascript">
 // Run after DOM ready
 $(function()
 {
  // When they click the Log In button
  $("#btnHashPassword").click(function()
  {
   getSaltAndNonce();
  });

  // If they hit the enter button, run the same hashPassword method
  $('#loginForm').bind('keypress', function(e)
  {
   if (e.keyCode == 13)
   {
    $(this).find("#btnHashPassword").click();
   }
  });
 });
</script>

I'm using the jsSHA library to do the hashing, jQuery 1.5.2 to handle the AJAX request and a custom ajax.js file to do the main work. I've chosen SHA512 for this example but there are other hash types available in the library and PHP as well.

I use some basic CSS and icons to show processing/success/error messages on the page. Here's the contents of the ajax.js file which gets the nonce and salt from the PHP page then hashes the salt password and nonce:

/*
 Get the salt related to the user's account and generate a random nonce value to prevent
 replay attacks. Show a status message as the request is processing.
*/
function getSaltAndNonce()
{
 // Show loading image
 var msg = '<span class="statusSuccess">Processing</span>' +
      '<img src="/public/images/loading-email.gif">';
 $('#requestStatus').html(msg);

 // Get salt and nonce from ajax json request to get-user-salt.php
 $.ajax({
  data: {username: $("#username").val()},
  dataType: 'json',
  success: function(data)
  {
   // Hash the user's password with the salt and nonce then submit the form
   hashPassword(data.salt, data.nonce);
   $("#loginForm").submit();
  },
  error: function()
  {
   // Show an error message on the page
   msg = '<img src="/public/images/error-icon.png"> ' +
      '<span class="statusError">Error contacting server, please try again.</span>';
   $('#requestStatus').html(msg);
  },
  url: '/private/ajax/get-user-salt.php'
 });
}

/*
 Hash the entered password with the salt and nonce so the plain text is not known to anyone else
 and is encrypted while it is being sent to the server. The nonce prevents replay attacks.
*/
function hashPassword(salt, nonce)
{
 // Get password value from form
 var password = $('#password').val();

 if (password != "")
 {
  // Create a hash of the salt and password
  var shaObj = new jsSHA(salt + password, 'ASCII');
  var saltPasswordHash = shaObj.getHash('SHA-512', 'HEX');

  // Create a hash of the salt and password and nonce
  shaObj = new jsSHA(saltPasswordHash + nonce, 'ASCII');
  var saltPasswordNonceHash = shaObj.getHash('SHA-512', 'HEX');

  // Store final hash in the form so it is sent to the server
  // Clear actual password value so it is not sent at all
  $('#hashedPassword').val(saltPasswordNonceHash);
  $('#password').val('');
 }
}

Here's the PHP page that the AJAX request fetches. It uses my own custom database class to run a prepared query against the database, you can swap this out with your own. You can see it outputs the JSON data directly:

<?php
$jsonOutput = array('salt' => null, 'nonce' => null);

if (isset($_GET['username']) && (!empty($_GET['username'])))
{
 $username = $db->clean($_GET['username']);
 
 // Check to see there isn't already another user with same username
 $params['username'] = $username;
 $result = $db->preparedSelect('select salt from users where username = :username limit 1', $params);

 // If the query returns a result then a user already exists so show an error
 if (($result !== false) && ($db->getNumRows() > 0))
 {
  foreach ($result as $key => $val)
  {
   $salt = $val['salt'];
  }

  // Create a random value to avoid replay attacks, store in session
  $nonce = $lib->createSalt();
  $_SESSION['nonce'] = $nonce;

  $jsonOutput['salt'] = $salt;
  $jsonOutput['nonce'] = $nonce;
 }
}

// Output JSON
header("Content-type: text/plain");
echo json_encode($jsonOutput);
?>

At this point if the user has clicked the Log In button and the AJAX request has succeeded it should have hashed the password and stored it in the hidden hashedPassword field on the form then submitted the form. Here's the checking code on the receiving page:

<?php
// Get form data and sanitise it
$username = $db->clean($_POST['username']);
$formPassword = $db->clean($_POST['hashedPassword']);

// Get data from form
if (empty($_POST['username']) or empty($_POST['hashedPassword']))
{
 header('location:login.php?status=requiredFieldsEmpty');
 exit();
}

// Get user's hashed salted password from database
$params = array('username' => $username);
$result = $db->preparedSelect('select password from users where username = :username limit 1', $params);

if (($result !== false) && ($db->getNumRows() > 0))
{
 foreach ($result as $key => $val)
 {
  $password = $val['password'];
 }
}

// Checks form password (passwordHash+nonce) against password (password+salt) hash in database
if ($lib->checkHashedNoncePassword($formPassword, $password, $_SESSION['nonce']))
{
 // Success - log user in and redirect to main page
}
else {
 // Incorrect password - log attempt and redirect back to login page
}
?>

There's also a small method in a class I use for the comparison. You could put it in the page if you wanted:

// Check the hashed nonce password sent by the user against the database hashed password and nonce
 public function checkHashedNoncePassword($formPassword, $hashedPassword, $nonce)
 {
  $passwordHashNonce = hash('sha512', $hashedPassword.$nonce);
  return ($formPassword == $passwordHashNonce) ? true : false;
 }

That's it! Nobody should be able to easily sniff the plaintext password as user's are logging in now, they'll have to resort to more complicated attacks like MITM. If anything it's better than sending them across the internet as plain text like most sites do. Make sure you've got some protection for session fixation and session hijacking as well. I'm considering implementing something else for the register and edit-password pages to protect the password in transit as well but will need to figure out how that will work.

If you've got any questions or suggestions for improvement let me know in the comments.