Secure Login and Registration with no SSL

Before I start, I should mention that SSL is the right way to secure your transport layer. If you're looking for an alternative workaround, you should have a very good reason for dismissing SSL. In almost all cases, SSL is the best and most secure way to do this. I should also mention that I'm not a security expert or a cryptographer. While I think the method described below gives an acceptable level of security for non-critical applications, I could be wrong. Use it at your own risk!

Now that the boring stuff is out of the way ( last chance to go back to SSL), here's how to implement a secure login and registration web application using Javascript, PHP, SHA-512, RSA and a few free libraries.

In a Nutshell
The method in a nutshell is as follows:
1. Use an asymmetric cipher such as RSA to generate a public/private key pair.
2. For the registration process, use the public key in the client-side to encrypt the password before sending it over the wire.
3. On the server-side, use the private key to decrypt the password to it's original form.
4. Take a hash of the password, say SHA-512, and store it in the DB.
5. For the login process, on the server-side, when generating the login page, generate a random salt and send it through as a hidden input element, also storing it in a session variable.
6. On the client-side, the user fills in his or her username and password and clicks Submit.
7. Client-side script takes a hash of the password, then concatenates this hash with the random salt, hash them together and send it through.
7. On the server-side, retrieve the password hash from the DB, concatenate with the salt in the session, hash them together and and compare with the input. If it matches, login is ok.

Implementation Step 1: Required Libraries
For the PHP back-end, you'll need an RSA library. I used this simple drop-in class (alternate link). Add it to your path and include it in whatever pages you wish to call it from.

As for the Javascript front-end, you'll need an RSA library and a SHA-512 library. I used the RSA scripts from this page (here's a backup zip) and the SHA-512 library from here (or here) . Add all these to the <head> section of the registration and login pages.

You'll also need an RSA key generator. This can be found on the same page as the JS RSA library here (or here).

Implementation Step 2: Generating Keys and Helper Functions
Generate an RSA public/private key pair using the key generator from above. Copy the values from the key generator, storing the private key & modulus in a PHP page, and the public key and modulus in a javascript page. For example, the following is a PHP snippet with some example keys and a few helper functions you'll need:
<?php
$KEY_PUBLIC_E = '2409978734715631029880355260357441432664472032526876114227569784495794566751510069512591219556109782576674888272112906199651289756735002819108142202823793';
$KEY_PRIVATE_D = '5752800841413850563159118480547357578024044378344797293929384567931284348206528886046316099556559729756833177438328626005195361420380997379897060720530009';
$KEY_MODULUS_M = '6986788427462396708680671924498503369468453574377731427576537302498134353975371027446862902478001472164969772640394368449022746485345874915158321127688709';
$KEY_LENGTH = 512;

function decrypt($hex_string) {
include "rsa.php";
$dec = rsa_decrypt(hex2binStr($hex_string), $KEY_PRIVATE_E, $KEY_MODULUS_D, $KEY_LENGTH_M);
return $dec;
}

function hex2binStr($hex_str) {
$bin = '';
for($i = 0; $i < strlen($hex_str); $i+=2) $bin .= chr(hexdec($hex_str[$i] . $hex_str[$i+1]));
return $bin;
}
Notice that the keys are copy-pasted from the top textbox of the RSA generator. These are in decimal-string notation. The decrypt function is what you'll use to decrypt the RSA password sent over from Javascript. The input is a hex-string representation of the encrypted message. The PHP RSA class requires the encrypted message to be in binary-string notation, so we use a little conversion function to do the transformation before invoking the RSA decrypt function.

On the Javascript front, include all the libraries you've downloaded above in a header and use the following snippet as a helper-function. Notice the keys are again copied from the RSA generator, but this time they're in hex-string notation. You can copy this snippet directly from the RSA generator, but remember to leave the private key blank! This is your secret key, only the server should have it.
<script>
my_rsa_encrypt = function(str) {
setMaxDigits(67); //1024 bit key
// Put this statement in your code to create a new RSA key with these parameters
var key = new RSAKeyPair(
"2e03bb0d862410c628ba5c8751fa7909653be3bd6d3f002a5887df33dd2cafb39f7f413c871992cf042996edf31cf356def1de462ae0241a3a1c279d90c92471",
"", //don't supply the private key, we're using client only for encryption, server uses private key to decrypt'
"8566b5e009f8154b7b14fa517a69b501422896dcf6753ed8c53903a2286e9a6ab782067d33a10ad9cbd8cec989d339936628db585c40c45531b493010a170a05"
);
var enc = encryptedString(key,str + "\x01");
return enc;
}
</script>

As a final configuration issue, we need to make a small source-code change to the PHP rsa class. Replace the rsa_decrypt function at line 49 with the following:

function rsa_decrypt($message, $private_key, $modulus, $keylength)
{
$number = binary_to_number($message);
$decrypted = pow_mod($number, $private_key, $modulus);
$result = number_to_binary($decrypted, $keylength / 8);

$res = '';
for($i = strlen($result)-1; $i >= 0; $i--) {
if($result[$i] == "\x01") break;
$res .= $result[$i];
}
return $res;

//return remove_PKCS1_padding($result, $keylength / 8);
}
Notice that we just commented out the final line and replaced it with the for loop above it. The reason for this is a little bizarre, but basically when I tested encrypting string from the Javascript library and decrypting in PHP, I found that for some reason the result string was correct but came out backwards. This also caused the remove_PKCS1_padding function to fire some failed assertions. I didn't have the time to trace through the Javascript and PHP code to see where the implementations differed, so this is just a hack to make the two play well together.

Implementation Step 3: Securing the Registration
Now that all the setup is out of the way, securing the registration process is very easy. On the client-side registration page, just have a form with all the relevant input fields and a password field. When the user clicks the Submit button, trigger a Javascript function that takes the value from the password field and runs it through the my_rsa_encrypt function. Then either paste this encrypted password in a new hidden field and clear the clear-text password, or use AJAX to send across all the registration details and encrypted password only.

On the PHP back-end, in the function that processes the registration request, simply run the encrypted password through our decrypt helper function. The return of this should be the original plain-text representation of the user's password.

For added security, passwords should be stored as hashes in the database. Using SHA-512 in PHP 5.2+ is easy:

$hash = hash('sha512', $password);

Implementation Step 4: Securing the Login
On the server-side, when generating the login page, generate a random salt in PHP such as:
hash('sha512','' . time() . rand());
Store this in a session variable and send it through to the client as a hidden input field in the login form.

On the client-side, when the user enters his or her username and password and clicks Submit, trigger a Javascript function that takes a hash of the password, concatenates this with the salt from the hidden input field, takes another hash of the pair and sends it through to the server along with the username.

On the server-side in the function that processes the login, retrieve the user's password hash from the DB, concatenate with the salt from the session variable, hash the pair and compare with what was received over the wire. If they match, then login is ok.

Remember to delete or change the salt on every login attempt, or at least on every successful login. The salt must be unpredictable for this to work.

Pitfalls!
  1. Using large RSA keys is slow in Javascript. 512 bits is bearable, anything higher and the end-users browser will seem to hang while the encryption is taking place. A 512 bit RSA key is not considered strong for critical applications these days. You'd probably want 2048 or higher for say a banking website.
  2. This method only secures the login and registration pages, and only the passwords from these pages. Everything else is still sent in plain-text. So if a user is accessing private information after they've logged in, this method does absolutely nothing for you.
  3. You have a single private/public key pair for securing all your passwords, i.e. a master password. If you leak the private key, all future registrations and logins will be compromised. I had a quick look at how to dynamically create a new key-pair on every request but gave up. Generating RSA keys can be very computer intensive as well so it would make your server vulnerable for Denial-of-Service attacks.
  4. Storing the passwords as hashes in the database is useless in this case. If the database is compromised and the hashes are leaked, then someone can authenticate themselves simply by hashing the password hash together with the salt without needing the original password. You can instead just encrypt the password via javascript the same as for registration and send it through un-hashed, but it's a little slower and if the private key gets compromised you're done for.

Comments

Popular posts from this blog

Wkhtmltopdf font and sizing issues

Import Google Contacts to Nokia PC Suite

Can't delete last blank page from Word