This is a tutorial for how to authenticate users when you have an online service without SSL support.
Basic scenario
You have an online service that requires people to register and log in to use correctly. However, due to any number of issues, you do not have SSL support available for the service. How do you protect your users' passwords when they are registering / logging in AND make it so that it is harder for someone to impersonate them? I will give an overview, followed by code (or pseudo-code) and explanations, as appropriate.
Prerequisite knowledge
You should know what PHP and Javascript are. You should have an understanding of what hashing is, particularly cryptographic hashes such as MD5, SHA-1, and so on.
Certain hashes, including cryptographic hashes, are "one-way", i.e. there is no feasible mathematical way to un-hash a string in order to retrieve the original data. Regardless, there are things attackers can try to find your original text. In contrast, encryted text can be decrypted, given the same key.
Potential attacks
Before discussing techniques, it makes sense to see what kinds of attacks are possible on your services and how they work. This will help understand why certain techniques are better than others.
Dictionary attacks
In this attack, the attacker sits down with a huge list of words and tries them all against a hash function until he finds a match. This attack is a more refined version of a general brute force attacks. In a brute force attack, the attacker will systematically try every character combination within a given key space (combination of usable characters * (1 .. maximum length)). In this case, the attacker will use the most common words that have a chance of succeeding. In the majority of cases, these words come from English dictionaries. The reason this attack is successful is that people tend to choose passwords that are
short (<= 7 characters) words from the dictionary simple variations on dictionary words (appending a digit, misspelling by one letter, etc.)
Rainbow tablesRainbow tables are lists of plaintext passwords and their hashes. In essence, the attacker is precomputing hashes for a large selection of potential passwords. In order to crack your user's password, they will simply compare the hash against the ones in their table. If they find a match, the corresponding plaintext password is made available to the attacker. Please note that this is NOT decryption or dehashing (despite names like MD5 Decrypter). It is simply a huge lookup table. By creating such a table, an attacker is trading off time for memory. These attacks are preferred when going against multiple sites, since the hashes only have to be calculated once.
Brute force
This is the most time-consuming attack of the ones presented here. The attacker writes an algorithm which checks every character combination within a given key space in the hopes of finding a match. Even with regular English words, this attack takes a long time and a lot of computing power. Making a long password and adding symbols, digits and other special characters makes this attack essentially impossible to implement in practice.
Assumptions we can safely make
Given what we know about the above 3 attacks, there are some basic assumptions we can make on the kind of security we want to implement. We want to encourage users to not use simple passwords by enforcing good password selection in the code. In addition, we don't want to use encryption to protect passwords. The reason is simple: as stated above, anything that has been encrypted can be decrypted. The attacker just needs to get a hold of the key to get a hold of the plaintext password. So, we definitely want to use hashes whenever dealing with passwords.
NOTE: The logic used in the following 2 sections (Registration and Authentication) is not representative of how you would do it in the real world. For that, go to the section called Bringing it all together.
Registration
The very first problem we have to deal with is registration. Basically, we want to ensure that the user's password is protected at all times.
Obviously, transmitting the username / password in the clear to the back-end is out of the question. Any attacker who is watching a site / packet sniffing along the path can get a hold of the information without having to do almost any work.
The second option is to hash the password before sending it to the server. This avoids the problem of revealing the password to attackers. As was revealed recently (Do you use the same password for every website?), a large percentage of internet users like to reuse their passwords on each site. This means that, on average, you are keeping 1/3 of your users safe from attacks at other online services. This is done easily in PHP using code similar to:
$hashpw = hash('sha256', $pw);The third option is to "salt" the password before hashing it. While this suffers from the same problem as the previous option, it defeats the Rainbow table attack. This is especially true since each user will (or, at least, should) have their own unique salt. So, what is salting? Salting is basically taking an additional string and appending it to the password before hashing it. For example:
$hashed_password = hash('sha256',$password.'this is a salt');But, as I just mentioned, the salt should be unique for each user. Otherwise, there is no purpose to adding the salt. Here is code to create a salt that is somewhat more secure:
// $salt = hash('sha256', uniqid(mt_rand().$_SERVER['REMOTE_ADDR'].$_SERVER['REQUEST_TIME'], true)); // you get the idea
$hashpw = hash('sha256', $pw.$salt); linenums:0'>$salt = hash('sha256', uniqid(mt_rand(), true));// $salt = hash('sha256', uniqid(mt_rand().$_SERVER['REMOTE_ADDR'].$_SERVER['REQUEST_TIME'], true)); // you get the idea$hashpw = hash('sha256', $pw.$salt); The salt must be stored in the database along with the hashed password so that the password can be authenticated every time.
In addition to salting a password, we will introduce one more wrench to confound attackers. This is the "pepper." The concept of a pepper is identical to that of a salt. However, instead of storing it on the database, we store it in a regular file on the server. Why is this? It's to simply make it harder for someone to get into your systems. Let's say an attacker is able to get access to your database. Without also getting access to your server's file system, they will be unable to get a hold of the pepper. So, how do we use this? Here's a small code sample:
$salt = calc_salt(); // such as with the hash() above$pepper = read_pepper_from_disk();$hashpw = hash('sha256', $pw.$salt.$pepper);It's that simple. The way a salt differs from a pepper is that the pepper is the same for all users. This is because if the pepper is unique for all users, you have to maintain separate pepper files for each user, defeating the purpose of having a database in the background. We also keep the salt static to avoid having to recalc potentially tens of thousands of passwords for each change. Plus, if an attacker is able to get your pepper, it probably means she has access to your file system and you are in deep trouble anyway.
For now, without extensions, this is about the best PHP can do without using more complicated encryption methods like public/private key pairs. PHP 5.3 will include OpenSSL functions, which include a much better random number generator for additional security.
Authentication
The next challenge we face is authentication, i.e. logging an user in safely when they revisit your site. Authentication works exactly like registration, but with less calculations. Here is the basic concept:
if ($hashpw == read_user_hashedpw_from_DB()) {
return 'AUTHENTICATED';
} else {
return 'REJECTED';
} linenums:0'>$salt = read_user_salt_from_DB();$pepper = read_pepper_from_disk();$hashpw = hash('sha256', $pw.$salt.$pepper);if ($hashpw == read_user_hashedpw_from_DB()) { return 'AUTHENTICATED';} else { return 'REJECTED';} Since the various functions above will be different for each site, implementation is left to the reader. In addition to doing all this, here is an advanced technique you can use to really stump attackers. It is called dynamic salting. The theory behind it sounds complex, but it is quite simple and will force attackers to restart their attacks each time the user logs in. The idea is to change the salt each time a user logs in. While this technique works best if an user logs in often, the process does not become less safe just because we use it for an user who only logs in infrequently. Here is the concept (in pseudo-code):
update_user_salt_in_DB($userid, $new_salt);
update_user_hashedpw_in_DB($userid, $new_hashpw);
} linenums:0'>if ($login_is_successful) { $new_salt = calc_salt(); // using the same function as for registration. $new_hashpw = hash('sha256', $pw.$new_salt.$pepper); update_user_salt_in_DB($userid, $new_salt); update_user_hashedpw_in_DB($userid, $new_hashpw);}
Bringing it all togetherIf you've hung around so far, here is the part where I talk about how to implement this technique step-by-step. There are steps not addressed above, which will be dealt with here (such as how to get the password from the browser to the server without it being visible to everyone). I am showing the steps for getting authentication working. The steps for registration are almost identical.
Step 1 - In the browser
On the web page, have 2 forms: 1 visible form with fields for the username and password, one with no visible fields that can be used to submit the information to a PHP script. Put all this into a file called login.php.
Some PHP in support of dynamic salting:
?> linenums:0'><?php$new_salt = hash('sha256', uniqid(mt_rand(), true));?> Form 1 (HTML):
<form id="loginVisible" action="#" method="post"><input type="text" name="username" /><input type="password" name="password" /><input type="button" name="submit" value="Login" onclick="login();" /></form>Form 2 (HTML):
<form id="submitAuthenticate" action="authenticate.php" method="post"><input type="hidden" name="username" /><input type="hidden" name="securepw" /><input type="hidden" name="salt" /><input type="hidden" name="newsaltpass" /></form>JS login() script:
<!-- public domain SHA-256 JS implementation --><script type="text/javascript" src="benjaminjohnston.com.au/sha256.js; /><script type="text/javascript">var login_form;var xml_http;var url;var current_salt;var current_hashpass;function login() { login_form = document.getElementById("loginVisible"); xml_http = new XMLHttpRequest(); url = "get_salt.php?u=" + login_form.username.value; url = url + "&sid=" + Math.random(); xml_http.onreadystatechange = state_change; xml_http.open("GET", url, false); xml_http.send(null);}function state_change() { if (xml_http.readyState != 4) { alert("Something went wrong with XMLHTTPRequest!"); return false; } current_salt = xml_http.responseText; current_hashpass = sha256.process(login_form.password.value + current_salt); var submit_authenticate = document.getElementById("submitAuthenticate"); submit_authenticate.username.value = login_form.username.value; submit_authenticate.securepw.value = current_hashpass; submit_authenticate.salt.value = <?php echo $new_salt ?>; submit_authenticate.newsaltpass.value = sha256.process(login_form.password.value + submit_authenticate.salt.value); submit_authenticate.submit();}</script>
On the back-endpepper.php:
$pepper = "0123456789ABCDEF";get_salt.php:
include 'mysql_init.php'; // You need to initialize the connection to mysql, this is left as an exercise to the reader
// mysql_init(); // Initialize the DB connection here, connect to correct DB, and so on.
$query_result = mysql_query("SELECT users.salt FROM users WHERE users.username = " . $_REQUEST["u"]);
$result_array = mysql_fetch_array($query_result, MYSQL_NUM);
echo $result_array[0];
mysql_free_result($query_result);
// clean up mysql connection here
?> linenums:0'><?phpinclude 'mysql_info.php'; // You should have your Mysql IP, Port, Username, Pass in this.include 'mysql_init.php'; // You need to initialize the connection to mysql, this is left as an exercise to the reader// mysql_init(); // Initialize the DB connection here, connect to correct DB, and so on.$query_result = mysql_query("SELECT users.salt FROM users WHERE users.username = " . $_REQUEST["u"]);$result_array = mysql_fetch_array($query_result, MYSQL_NUM);echo $result_array[0];mysql_free_result($query_result);// clean up mysql connection here?> authenticate.php:
include 'mysql_init.php'; // You need to initialize the connection to mysql, this is left as an exercise to the reader
// mysql_init(); // Initialize the DB connection here, connect to correct DB, and so on.
include 'pepper.php';
$entered_password = hash('sha256', $_REQUEST["securepw"].$pepper);
$query_result = mysql_query("SELECT users.password FROM users WHERE users.username = " . $_REQUEST["username"]);
$result_array = mysql_fetch_array($query_result, MYSQL_NUM);
$db_password = hash('sha256', $result_array[0] . $pepper);
mysql_free_result($query_result);
// Dynamic salting at work.
if ($entered_password == $db_password) {
$new_salt = $_REQUEST["salt"];
$new_salt_pass = $_REQUEST["newsaltpass"];
mysql_query("UPDATE users SET users.salt=" . $new_salt . ", users.password=" . $new_salt_pass . " WHERE users.username = " . $_REQUEST["username"]);
// clean up myql connection here.
} else {
header("Location linenums:0'><?phpinclude 'mysql_info.php'; // You should have your Mysql IP, Port, Username, Pass, etc. in this.include 'mysql_init.php'; // You need to initialize the connection to mysql, this is left as an exercise to the reader// mysql_init(); // Initialize the DB connection here, connect to correct DB, and so on.include 'pepper.php';$entered_password = hash('sha256', $_REQUEST["securepw"].$pepper);$query_result = mysql_query("SELECT users.password FROM users WHERE users.username = " . $_REQUEST["username"]);$result_array = mysql_fetch_array($query_result, MYSQL_NUM);$db_password = hash('sha256', $result_array[0] . $pepper);mysql_free_result($query_result);// Dynamic salting at work.if ($entered_password == $db_password) { $new_salt = $_REQUEST["salt"]; $new_salt_pass = $_REQUEST["newsaltpass"]; mysql_query("UPDATE users SET users.salt=" . $new_salt . ", users.password=" . $new_salt_pass . " WHERE users.username = " . $_REQUEST["username"]); // clean up myql connection here.} else { header("Location:login.php?error=".urlencode("Failed authentication")); exit;}?>
Pros
The user's password is safe. It is never transmitted in the clear over the network. You are doing quite a lot to discourage attackers from getting into your site. The new salt generation can also be used for generating strong Session IDs for PHP sessions (a topic I didn't cover here).
Cons
All communication is occurring in the open, i.e. anyone sniffing packets will be able to see what is going on. If the user's data is intercepted during the initial registration, it will be much easier for someone to impersonate them.
To Do
Modify the functions so that if the user is registering, the $new_salt variable becomes the current salt. Add error checking. Wrap queries to mysql to protect against SQL injection attacks.
SummaryThis is, I hope, an useful overview of how to protect your users' information when SSL is not available. Remember, if your site contains sensitive information, you need to use SSL, some form of public key encryption or other method to ensure the information is not leaked.
References
https://en.wikipedia.org/wiki/Main_Page
http://www.w3schools.com/XML/xml_http.asp
http://www.codingforums.com/javascript-programming/142025-xmlhttprequest-wont-work.html
http://forums.xisto.com/no_longer_exists/
http://forums.xisto.com/no_longer_exists/
http://forums.xisto.com/no_longer_exists/
http://forums.xisto.com/no_longer_exists/
http://forums.devnetwork.net/viewtopic.php?f=34&t=88685
http://forums.devnetwork.net/viewtopic.php?f=34&t=92271
Regards,
z.