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:

Public key is in the JavaScript external file, private key in PHP on the server

User enters their username and password into the form and clicks Log In button

The JavaScript runs and encrypts the password, storing it back in the text field

Form is submitted to server and the password is decrypted with PHP

Plain text password in PHP is salted & hashed then compared to salted hashed password in the database.

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:

User enters username and password and clicks Log In button

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)

This value is then compared to the hidden value passed through from the form.

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.

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:

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:

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.

7 comments:

Can you go over the DB requirements? Specifically, the table structure and brief description of each field? It seems you have a password field and a salt field; I'm assuming the password field is hashed, but I don't know what relation the salt is to the password, where it comes from, does it get updated, etc.

For the database I have 3 fields in the users table: username (varchar 50), password (varchar 128), salt (varchar 128). The salt field is a unique string generated for each user row in the database. The password field actually contains the sha512(salt + password). Hope that helps, let me know if any more questions.

I actually figured it out. Not only was I hashing the password by itself before hashing the salt + password, but I double-checked my DB table and saw that my password field was only varchar(64). Everything works now. Thank you so much.

You are right, I devised a better system which has a 'pepper' or global salt as well. This is a random 512 bit system wide constant which is not stored in the same database with the other salts but hidden in the code/HSM somewhere. This value is then hashed with the salt, then that hashed salt is sent to the client with the nonce. Little bit more complicated but it works.