Everybody loves the Web 2.0 style of no page loads and seamless transitions (almost everybody). But there are some security limitations placed on the xmlhttp callback objects that make life a little difficult sometimes. For instance, if your page originated on the server http://www.switchonthecode.com, you are only allowed to use the xmlhttp request object to request data from http://www.switchonthecode.com. If you try and request data from https://www.switchonthecode.com, the request will throw an exception and you'll get nowhere.
So, for instance, if you want to have your users authenticate over ssl, you either have to do a page reload, or your page has to be on ssl to begin with. Neither of those options is particularly attractive - doing a page reload destroys that nice illusion of seamlessness, and having the page on ssl all the time is a lot of overhead server-side.
A third option is to come up with a scheme to secure the data that needs to be secured without having to use ssl. And that is what we are going to take a look at today. Here, we are going to go over a (relatively) secure method of exchanging passwords over an unsecured http connection with only a little bit of extra work on the client and server side. First, I'm going to discuss how it will work in general (and what its main flaw is), and then I'll go over its implementation in code.
There are two main stages to this system: registration and authentication. The registration part is pretty simple. On the client side, the user will enter a password, and we will take the hash of that password. That hash will be transmitted to the server, where it will be stored (probably in a database).
Authentication is a little more complicated, but its not too bad. When the user goes to authenticate, the server creates a random number, stores it (probably in the session), and sends it to the user. The user will enter their password and it will be hashed. This hash will then be concatenated with the random number and hashed again. This double hash will then be transmitted to the server. On the server side, the server pulls out the password hash from the database and the random number from the session, concatenates them, hashes that value, and then compares that value with the value transmitted from the client. If the two values match, then we can authenticate the user.
The flaw here is with registration. If someone catches the registration transmission, they can get that original password hash. With that hash, they can probably manage to authenticate against the server, thereby impersonating the original user. They can't, however, ever get the user's actual password - because that value is never transmitted over the wire. The server doesn't even know it, it only knows that hash. And the beauty of a hash is that it is impossible to get the original value back out (if you want more info on how cryptographic hashing works, take a look at the Wikipedia article).
In many cases, this flaw is acceptable, because this risk of user impersonation is not a problem (i.e., perhaps there isn't any 'private' content on the site). In fact, if there is private content, then you should probably be using ssl anyway, so that that content is secured from eavesdroppers. But in the case where the only thing that really needs to be secured is the password itself, this method is perfectly acceptable (because, again, there is no way to get the original password).
So now that we know the method behind all of this, lets take a look at how we can actually implement it. Here, the client side work will all be done by javascript, and the server side will be in php. First things first - we need a hashing function. Now lucky for us, php already has a number of hashing functions, including the one we will be using - SHA1. Javascript, on the other hand, does not. Fortunately, a number of people have written SHA1 hashing functions for javascript, and we will be using one of them.
The following javascript SHA1 implementation is courtesy of Chris Veness and is licensed under the LGPL.
{
// constants [§4.2.1]
var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
// PREPROCESSING
// add trailing '1' bit to string [§5.1.1]
msg += String.fromCharCode(0x80);
// convert string msg into 512-bit/16-integer
// blocks arrays of ints [§5.2.1]
// long enough to contain msg plus 2-word length
var l = Math.ceil(msg.length/4) + 2;
// in N 16-int blocks
var N = Math.ceil(l/16);
var M = new Array(N);
for (var i=0; i<N; i++) {
M[i] = new Array(16);
// encode 4 chars per integer, big-endian encoding
for (var j=0; j<16; j++) {
M[i][j] = (msg.charCodeAt(i*64+j*4)<<24) |
(msg.charCodeAt(i*64+j*4+1)<<16) |
(msg.charCodeAt(i*64+j*4+2)<<8) |
(msg.charCodeAt(i*64+j*4+3));
}
}
// add length (in bits) into final pair of 32-bit integers
// (big-endian) [5.1.1]
// note: most significant word would be
// ((len-1)*8 >>> 32, but since JS converts
// bitwise-op args to 32 bits, we need to simulate
// this by arithmetic operators
M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32);
M[N-1][14] = Math.floor(M[N-1][14]);
M[N-1][15] = ((msg.length-1)*8) & 0xffffffff;
// set initial hash value [§5.3.1]
var H0 = 0x67452301;
var H1 = 0xefcdab89;
var H2 = 0x98badcfe;
var H3 = 0x10325476;
var H4 = 0xc3d2e1f0;
// HASH COMPUTATION [§6.1.2]
var W = new Array(80); var a, b, c, d, e;
for (var i=0; i<N; i++) {
// 1 - prepare message schedule 'W'
for (var t=0; t<16; t++)
W[t] = M[i][t];
for (var t=16; t<80; t++)
W[t] = ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1);
// 2 - initialise five working variables
// a, b, c, d, e with previous hash value
a = H0; b = H1; c = H2; d = H3; e = H4;
// 3 - main loop
for (var t=0; t<80; t++) {
// seq for blocks of 'f' functions and 'K' constants
var s = Math.floor(t/20);
var T = (ROTL(a,5) + f(s,b,c,d) + e + K[s] + W[t])
& 0xffffffff;
e = d;
d = c;
c = ROTL(b, 30);
b = a;
a = T;
}
// 4 - compute the new intermediate hash value
// note 'addition modulo 2^32'
H0 = (H0+a) & 0xffffffff;
H1 = (H1+b) & 0xffffffff;
H2 = (H2+c) & 0xffffffff;
H3 = (H3+d) & 0xffffffff;
H4 = (H4+e) & 0xffffffff;
}
return H0.toHexStr() + H1.toHexStr() + H2.toHexStr() +
H3.toHexStr() + H4.toHexStr();
}
//
// function 'f' [§4.1.1]
//
function f(s, x, y, z)
{
switch (s) {
case 0: return (x & y) ^ (~x & z); // Ch()
case 1: return x ^ y ^ z; // Parity()
case 2: return (x & y) ^ (x & z) ^ (y & z); // Maj()
case 3: return x ^ y ^ z; // Parity()
}
}
//
// rotate left (circular left shift) value x
// by n positions [§3.2.5]
//
function ROTL(x, n)
{
return (x<<n) | (x>>>(32-n));
}
//
// extend Number class with a tailored hex-string method
// (note toString(16) is implementation-dependant, and
// in IE returns signed numbers when used on full words)
//
Number.prototype.toHexStr = function()
{
var s="", v;
for (var i=7; i>=0; i--) {
v = (this>>>(i*4)) & 0xf; s += v.toString(16); }
return s;
}
Now that we have the SHA1 hash in both php and javascript, we can go ahead and write the actual implementation. First off, the xmlhtttp callback function:
{
var xmlObj =null;
if(window.XMLHttpRequest)
xmlObj = new XMLHttpRequest();
else if(window.ActiveXObject)
xmlObj = new ActiveXObject("Microsoft.XMLHTTP");
if(xmlObj == null)
return false;
xmlObj.onreadystatechange =
function(){
if(xmlObj.readyState == 4)
{
callbackFuntion(xmlObj.responseText);
}
}
xmlObj.open ('GET', url, true);
xmlObj.send ('');
return true;
}
This function is just a simple wrapper around the xmlhttp object. It takes as arguments the url to callback to, and the javascript function to call with the callback result. It returns true or false depending on whether it is able to start the callback. A return of true does guarantee completion - it just means the callback has been started. We will be using this function to do the callbacks for both registration and login.
Now onto the code for registration client-side:
function RegisterClicked()
{
var name = document.getElementById('registerName');
var pass = document.getElementById('registerPass');
var url = loginURL + "?pass=" + sha1Hash(pass1.value)
+ "&name=" + name.value + "&Register";
if(!doCallback(url, RegisterResponse))
alert("Error!");
}
function RegisterResponse(responseText)
{
alert("Registered!");
}
First off, there will probably be some sort of constant url to the php file on the server we will be calling back to. Here, that is a global variable called loginURL. The two functions following that variable are extremely simple. The first one, RegisterClicked(), would probably be hooked to some button or link on the page. Calling it just grabs the new user name and password from some text boxes on the page and creates a url. As you can see, it calls the SHA1 hash function on the password before it is even put in the url. This url is then posted back to the server, with the function RegisterResponse set to be called with the result. The RegisterReponse function, as it stand right now, does nothing, but in reality it should be checking the result to make sure there were no errors in registration.
Now lets take a look at the client-side code for logging in:
{
if(!doCallback(loginURL + "?Challenge", ChallengeResponse))
alert("Error!");
}
function ChallengeResponse(challenge)
{
var name = gID('loginName');
var pass = gID('loginPass');
var url = loginURL + "?name=" + name.value + "&pass="
+ sha1Hash(sha1Hash(pass.value)+challenge);
if(!doCallback(url, LoginResponse))
alert("Error!");
}
function LoginResponse(response)
{
if(response == "Yay")
alert("Logged In!");
else
alert("Login Failed.");
}
Here, the login process is broken into two major parts: getting the challenge value, and then the actual authentication. The function LoginClicked (which is probably called by some link or button on the page) immediately goes and requests a challenge value from the server. When the server responds, the ChallengeResponse function will be called, and the argument challenge will hold that challenge. Now, we can build the url - as we needed the challenge value to build the correct hash of the password. We callback using this new url, and the response goes to the LoginResponse function. And here , if the response is "Yay", apparently the server has successfully logged the user in.
I'm betting, at this point, you want to know what this all looks like on the server side. Well, lets take a look:
session_start();
/////
//Probably some database includes here
/////
if(isset($_GET['Register'])) //Register Block
{
$user = User::createUser($_GET['name'], $_GET['password']);
$user->save();
echo "Registered!";
}
else if(isset($_GET['Challenge'])) //Challenge Block
{
$_SESSION['challenge'] = mt_rand()."".mt_rand();
echo $_SESSION['challenge'];
}
else //Login Block
{
$user = Database::getUser($_GET['name']);
if(strtolower(sha1($user->Password.$_SESSION['challenge']))
== strtolower($_GET['password'])
{
echo "Yay!";
}
else
{
echo "No!";
}
}
?>
Now, there are a number of details missing here, mostly because theres no need to get into the details of php and databases. But the basics are here, and should make sense. So, based on the arguments on the callback url, we either enter the Register, the Challenge, or the Login block. For Register, we take the new name and password, create a "user" (something, perhaps, defined elsewhere in php code), and save that user to the database. For Challenge, we create a decently long random string, save it in the session, and shoot it off to the client. And then for Login, we get the user out of the database based off of the given name, and then see if the password is valid. We call strtolower on both hashes because both are hexadecimal strings, and it is possible that one string has the letters capitalized (like A3F1543ED287) while the other is lowercase (a3f1543ed287). Since they are equivalent hashes either way, we just lower the case on both so we can do a string comparison. And if they match, the password is valid, so we can authenticate the user!
And that about covers it. You won't be able to lift the code directly off this page and use it without modification - because you will need to write the database functionality for php and you will probably want to flesh out the communication between server and client so that errors are communicated more appropriately. But I hope you find the technique useful and easy to use. If you have any questions, feel free to leave them in the comments.
08/15/2007 - 18:18
Excellent article, very nice!
10/14/2007 - 05:30
All you need to avoid the problem with the registration is change the way the you do the authentication. Take the password, concatenate the random number, hash both and then send that hash.
This way the only problem is with short passwords (which can be quickly bruteforced). To eliminate this problem, concatenate the password x times, until the length is greather than what you consider secure.
10/19/2007 - 04:32
Hi! I am writing a game in Javascript (Silverlight). At the end, this script call a website and give the highscore.
There is a problem with this method: You can pause the request (with Fiddler) and change the highscore. Is there a way to bypass this problem? At begin of the game, I send a code, at the end the game send it back, but a hacker can easly change the request and send a higher highscore! Thanks, Adriano
12/21/2009 - 01:20
checksum!
10/24/2007 - 15:39
{
var myform=document.createElement("form");
myform.id = "LoginForm";
myform.name = "LoginForm";
myform.method="POST";
myform.action="https://MySite/LoginPage.aspx";
var userName = document.createElement("input");
userName.type="hidden";
userName.name="UserName";
userName.value=user;
myform.appendChild(userName);
var userPassword = document.createElement("input");
userPassword.type="hidden";
userPassword.name="Password";
userPassword.value = password;
myform.appendChild(userPassword);
document.appendChild(myform);
document.forms["LoginForm"].submit();
}
This will always work.
10/24/2007 - 15:40
This is a clean way of implementing as well.
10/24/2007 - 16:03
But won't that reload the page?
10/25/2007 - 01:48
Very nice code snippet. Well I will try that on my site wich needs a secure part of site for special users. THX
02/22/2008 - 14:49
That is really nice and kind of you, man! I am a beginner, and it is a very good read.
03/05/2008 - 13:47
Thanx man, it is a real good thing.
05/28/2008 - 18:00
Neat, I came up with the same thing a year or so ago it is a very ingenious technique.
It's not full proof from Brute Force attacks because of the ability of (someone watching wire transmissions) to take the session random number then they can just use the same hash technique with a dictionary attack to get the transmitted password.
If you are going to buy SSL for protecting passwords this solution is much better.
07/13/2008 - 09:37
I find the complete implementation. I am not an expert and i have much problem to implement this...Thanks
07/31/2008 - 07:23
WARNING this technique is not safe at all!
Any javascript based encryption is non sense because if someone could intercept the communication, it could change the javascript code too (just imagine it change SHA(x) to return x)
Then it does get the password, can authenticate on the server, and you'll never see anything wrong!.
The only solution is to use methods that are implemented directly in the browser (so they can't be forged), and to my own knowledge, SSL is the only way.
07/31/2008 - 09:30
A good point, this technique does not protect you from man-in-the-middle attacks - it only protects you from sniffing attacks (like on an open wireless network).
I wish that the root certificate for a place with free ssl certificates (like CACert) was actually included with Firefox/IE - there would be no reason then _not_ to use ssl.
09/06/2008 - 16:25
@The Tallest It is easy enough to generate a self-signed certificate. From an encryption perspective, it is every bit as safe as a certificate from a major certifying authority.
What you are getting beyond that when using a certifying authority is some level of identity verification. The authorities attempt to verify the identity of the person or company that owns the domain.
As to the article in general, I don't think the inconvenience of reloading the page to enable SSL is worth using a much less secure JavaScript implementation. In any case, all you need to do is set up mod_rewrite to redirect http to https, and that problem is solved.
Encryption is hard. Stick with the tried and true methods developed by experts.
09/09/2008 - 09:13
From *purely* an encryption perspective, yes, a self signed certificate has at least as much strength than one from a CA, but unless the user on the other end has already been supplied a copy of your public key out of band, it's a complete waste of time.
There's nothing stopping someone who's MitMing you from using OpenSSL to spit out an "identical" (except for key, fingerprint, etc) self-signed certificate on the fly at the start of a session.
The solution here is nice, but I'd move the actual authentication data into POST variables. Sure, it won't stop someone from sniffing it, but it'll keep out casual readers of proxy logs (assuming they can then be arsed reverse engineering it and brute-forcing.. it's just another minor barrier).
Either way, this isn't a perfect solution (though even HTTP-HTTPS redirection can be MitMed - I mean, how many users would be able to explain to you how to tell if they were on a secure connection?), but it's still streets ahead of the vast majority of sites out there still using clear text password forms...
11/13/2008 - 20:26
I have been using this solution in scripts for years, I thought I was the only genius that came up with it. Now you have taken my thunder great job!
Na I am just teasing ya, I have always liked this method never employed it into AJAX but I do use it when an SSL cert is out of the question.
However I don't see how SSL is much more secure than using this method only using an algorithm like base64 instead of sha1. But I guess I will educate myself on the difference's at a later date.
Great Read, great refresher.
08/31/2009 - 13:40
In this "solution" the user's password *is* their hash. The hash is used to authenticate a user on the server.
Stealing the user's hash is just as useful as stealing the text they type in to generate the hash. Since it is the user's hash that will log anyone into the server ... why would an attacker care about the original text used to generate it? All that is important is the hash and, therefore, it effectively functions as the user's password.
Cryptographic hashes, when used in conjunction with passwords, are intended to obscure the user's actual password text from the server but still allow authentication. They are not intended to stop attackers from stealing those hashes and then pretending to be a user.
Any clear text sent across the wire that is used to authenticate a user can be stolen. It doesn't matter *what* that text is. You must encrypt the text to protect it from being stolen, i.e. use SSL.
Beginners, please do not use this "solution" for anything important ... it is not secure.
08/31/2009 - 14:11
From the article:
"In many cases, this flaw is acceptable, because this risk of user impersonation is not a problem (i.e., perhaps there isn't any 'private' content on the site). In fact, if there is private content, then you should probably be using ssl anyway, so that that content is secured from eavesdroppers"
12/21/2009 - 01:24
so what's the use of this then?
12/21/2009 - 10:03
When the server stores no private information. We used it on a project that required user authentication, however if someone were to gain access to your account, there was no important information that could be obtained. This method ensures that the user's actual plain-text password could not be stolen, however they could still use it to log into our service.
01/31/2010 - 10:13
I have found a problem. If i register with some hash eg. 123456789. If hacker gets this code(123456789) they can view source of login page, find the challenge and make the final login hash themselves. All they have to do then is send request to login. Does anyone now a way around this?
04/11/2010 - 12:23
Two ways around this. First is to only create accounts from an SSL-based page (or other secure/out-of-band method, such as directly on the SQL server - whereby password hashes are never transmitted un-salted.)
The second method is to only allow the salt to be used one time, and if a request comes in trying to use the same salt, then kill both that session and the session that used it in the first place. Because you can't tell which attempt is legit and which is a hack, it's best to kill it all and just make the user log in again. It's worth the inconvenience for the security increase.
07/21/2010 - 00:43
There is now an SSL implementation in javascript that might be of interest to the author and readers here: http://blog.digitalbazaar.com/2010/07/20/javascript-tls-1/2
06/15/2011 - 10:36
I have an alternate solution for this. It uses public key encryption using javascript to encrypt a login form prior to being sent to the server.
The project is currently over at https://github.com/jas-/jQuery.pidCrypt.
Here is the contents of the readme document in regards to features.
jQuery plugin to impliment RSA public key encryption
Utilizes the pidCrypt libraries for client public key encryption while the associated PHP class uses OpenSSL to generate the necessary private/public key pairs used by this plug-in
Fork me @ https://www.github.com/jas-/jQuery.pidCrypt
REQUIREMENTS:
jQuery libraries (required - http://www.jquery.com)
pidCrypt RSA & AES libraries (required - https://www.pidder.com/pidcrypt/)
jQuery cookie plugin (optional - http://plugins.jquery.com/files/jquery.cookie.js.txt)
OpenSSL < 0.9.8
PHP < 5.3
FEATURES:
HTML5 localStorage support
HTML5 sessionStorage support
Cookie support
Debugging output
METHODS:
Default: Uses public key to encrypt form data prior to sending
Sign: Uses public key to sign data being emailed to recipient
Encypt_sign: Uses public key to encrypt and send email to recipient
Authenticate: Uses PKCS#12 PEM encoded certificaes for passwordless authentication
OPTIONS:
storage: HTML5 localStorage, sessionStorage and cookies supported
callback: Optional function used once server recieves encrypted data
reset: Prevent local caching of public key (forces server requests)
debug: Appends debugging information
EXAMPLES:
DEFAULT USAGE:
Default usage using HTML5 localStorage $('#form').pidCrypt();
Default Using HTML5 sessionStorage $('#form').pidCrypt({storage:'sessionStorage'});
Default using cookies (requires the jQuery cookie plug-in) $('#form').pidCrypt({storage:'cookie'});
Example of using the callback method to process server response $('#form').pidCrypt({callback:function(){ console.log('foo'); }});
Disable local caching of public key $('#form').pidCrypt({cache:false});
Enable debugging output $('#form').pidCrypt({debug:true});
Using PKCS#7 email signing:
Using a PKCS#7 certificate for email signing $('#form').pidCrypt('sign');
Using a PKCS#7 certificate for email signing and sessionStorage $('#form').pidCrypt('sign',{storage:'sessionStorage'});
Using a PKCS#7 certificate for email signing and cookies (requires the jQuery cookie plug-in) $('#form').pidCrypt('sign',{storage:'cookie'});
Using a PKCS#7 certificate for email signing using the callback method to process server response $('#form').pidCrypt('sign',{callback:function(){ console.log('foo'); }});
Using a PKCS#7 certificate for email signing while disabling local caching of public key $('#form').pidCrypt('sign',{cache:false});
Using a PKCS#7 certificate for email signing while enabling debugging output $('#form').pidCrypt('sign',{debug:true});
Using PKCS#7 email encryption and signing
Using a PKCS#7 certificate for email encryption & signing $('#form').pidCrypt('encrypt_sign');
Using a PKCS#7 certificate for email encryption & signing and sessionStorage $('#form').pidCrypt('encrypt_sign',{storage:'sessionStorage'});
Using a PKCS#7 certificate for email encryption & signing and cookies (requires the jQuery cookie plug-in) $('#form').pidCrypt('encrypt_sign',{storage:'cookie'});
Using a PKCS#7 certificate for email signing using the callback method to process server response $('#form').pidCrypt('sign',{callback:function(){ console.log('foo'); }});
Using a PKCS#7 certificate for email encryption & signing while disabling local caching of public key $('#form').pidCrypt('encrypt_sign',{cache:false});
Using a PKCS#7 certificate for email encryption & signing while enabling debugging output $('#form').pidCrypt('encrypt_sign',{debug:true});
Using PKCS#12 certificate authentication
Using a PKCS#12 certificate for authentication $('#form').pidCrypt('authenticate');
Using a PKCS#12 certificate for authentication with sessionStorage $('#form').pidCrypt('authenticate',{storage:'sessionStorage'});
Using a PKCS#12 certificate for authentication with cookies (requires the jQuery cookie plug-in) $('#form').pidCrypt('authenticate',{storage:'cookie'});
Using a PKCS#12 certificate for authentication while using the callback method to process server response $('#form').pidCrypt('authenticate',{callback:function(){ console.log('foo'); }});
Using a PKCS#12 certificate for authentication while disabling local caching of public key $('#form').pidCrypt('authenticate',{cache:false});
Using a PKCS#12 certificate for authentication while enabling debugging output $('#form').pidCrypt('authenticate',{debug:true});
TODO:
Add PKCS#7 signed email validation
Add PKCS#7 email decryption and signed validation
Resovle issue with authenticate method regarding discrepencies with PKCS#12 certificates
Author: Jason Gerfen jason.gerfen@gmail.com License: GPL (see LICENSE)
10/21/2011 - 14:29
you forgot to salt your password on the database. Once you use the salt, you can not authenticate with a challenge word.
01/03/2012 - 10:16
In order to make brute force attacks less efficient, you can use Bcrypt instead of SHA1. There is a javascript version of Bcrypt and this is designed to be significantly slower than other crypt solutions. Which means that it is better for bruteforce and only adapted to small pieces of data (passwords mainly).
On the server side though, I don't know if there is a PHP version. There is one for Ruby. But I would assume so.
Thx for the article.
Very interesting,
Mig
Add Comment
[language] [/language]
Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]
See here for supported languages.
Javascript must be enabled to submit anonymous comments - or you can login.