Challenge-response authentication
03/15/2007
Permlink
Sometimes, you have a website that needs a secure login, but the site itself doesn't contain secure information. In other words, you want people to login, but once they do you don't need SSL to secure the information you're going to send them. No matter how trivial the site is, you don't want to send the user's password in clear text over the internet, because too many people use the same ID's and passwords everywhere they go. Your site may not contain any private information, but by sending the password in clear text, you might be helping some nefarious person abuse your user's password somewhere that it does matter, like Amazon.com.
Of course, you can always use SSL for your login, but setting up SSL is a lot of trouble for just a login screen. To do it right, you need a real certificate and that can run into money. The method presented here is easier than SSL, but a lot more secure than no security at all (it isn't theoretically ironclad, however: enough effort and computing power can crack it, at least in theory).
Fortunately, there is a way out, called challenge-response authentication (Wiki article here). This technique involves the website issuing a "challenge" (in our case, a random number) in response to which the user's browser issues a "response" that is based on the user's password, but is not itself the password. The password doesn't go over the internet, just the response. Since we use a mathematical function to generate the response that is a one-way function, a "man in the middle" can't back the function up and deduce the password. Don't worry, this sounds a lot more complicated than it is.
Step 1: Issue the challenge
The "challenge", in our case, is just a random number. It's important, however, that the server side remember the challenge it has issued to a given user. You can do this with cookies, I suppose, but that's not very secure. Probably your best bet is to tie the challenge to some server-side session state. Since I use a standard J2EE stack on Tomcat, I don't have to worry about this: Tomcat is tracking the session for me. Here's the code I use:
Random rand = new Random(System.currentTimeMillis());
Long challenge = new Long(rand.nextLong());
loginForm.setName("");
loginForm.setPassword("");
loginForm.setChallenge(Long.toHexString(challenge.longValue()));
session.setAttribute("challenge",
Long.toHexString(challenge.longValue()));
This is pretty standard stuff. I'm just getting a random long integer and sticking it into a form that's being sent to the user.
Step 2: Generate response
This part is a little more tricky, since we have to use Javascript to do the work. In the HTML of your login page, you will have a username/password form with a Submit button. You'll need to have the Submit button call your Javascript before the form is submitted. Something like this:
<input type="submit" value="Login" onclick="doMD5();">
As you can see, we're calling the "doMD5" Javascript function when the user clicks the Submit button. It's named "doMD5" because the mathematical function we're going to use is called "Message-Digest algorithm 5" (wiki article here). This algorithm takes each character of the source and cranks it through a series of mathematical operations. When it's done, it has generated a 128-bit result that is (nearly) unique to the original input. The chances that two passwords will turn out the same MD5 digest are astronomical.
Read the wiki article: there are ways to beat the MD5 algorithm. They're a lot of work, but it can be done. This isn't absolutely secure, just good enough for small-potatoes stuff that doesn't justify a lot of effort!
The easiest thing to do is use a toolkit that already has an MD5 algorithm (if you're experienced with Javascript, however, you can download a raw MD5 function here). I was already using the Dojo Toolkit and it has MD5 in its crypto section, so I used that. Here's the MD5 function in my Javascript that calls the Dojo Toolkit to do the work:
<script type="text/javascript" src="/scripts/dojo.js" ></script>
<script type="text/javascript">
dojo.require("dojo.crypto.MD5");
</script>
<script type="text/javascript">
function doMD5() {
try {
var pwfield = dojo.byId("password");
var pwd = pwfield.value;
var chfield = dojo.byId("challenge");
var challenge = chfield.value;
pwfield.value = dojo.crypto.MD5.compute(challenge + pwd, 1);
} catch (ex) {
alert(ex);
}
return true;
}
</script>
This is pretty easy: the first few lines just tell Dojo to load the "dojo.cripto.MD5" code from the server. The function gets the password field (if you don't use Dojo, there are lots of other ways to do this) and gets the password the user typed out of the field. Then it gets the "challenge" –that random number we generated and sent down before– and sends both strings to the MD5 function in Dojo, asking it to return the MD5 as a hexadecimal string.
This is the heart of this technique: the MD5 is "digesting" both the challenge and the password. The result will be different every time the user logs in because a different challenge will be sent down every time. Anyone listening in on this won't see the password, they'll see a different string of 128 bits of hex each time.
Lastly, the result of the MD5 calculation is stuffed back into the the password field. When the browser POSTS the form back to the server, it will send the result of the MD5 calculation, not the plain-text password.
Step 3: Validate the response
On the server side, we now have to check that the MD5 the user's browser has sent us contains the proper password. Since we've remembered the challenge we sent them and we have their password (looked up from our database), we compute the same MD5 and compare it to the one we sent down. If they match, the user typed in the proper password. Here's some Java code:
String pw = loginForm.getPassword();
String challenge = (String) session.getAttribute("challenge");
MessageDigest md5 = MessageDigest.getInstance("MD5");
String all = challenge + user.getPassword();
md5.update(all.getBytes());
String userDigest = byteArrayToHexString(md5.digest());
if(userDigest.equalsIgnoreCase(pw)) {
// successful login, do whatever you do next
} else {
// bad password, don't let them in!
}
// ---------------------
// here's the byteArrayToHexString function:
//
private String byteArrayToHexString(byte in[]) {
byte ch = 0x00;
int i = 0;
if (in == null || in.length <= 0) {
return null;
}
String pseudo[] = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9",
"A", "B", "C", "D", "E", "F" };
StringBuffer out = new StringBuffer(in.length * 2);
while (i < in.length) {
ch = (byte) (in[i] & 0xF0);
ch = (byte) (ch >>> 4);
ch = (byte) (ch & 0x0F);
out.append(pseudo[(int) ch]);
ch = (byte) (in[i] & 0x0F);
out.append(pseudo[(int) ch]);
i++;
}
return out.toString();
}
So, you get the MD5 digest the browser sent up in the form. You calculate your own (and turn it into hex digits) and compare them. I used the MD5 function from the "java.security.MessageDigest" package, but you can find them for many languages around on the internet.
There is one possible fly in the ointment. You may not have the user's password. If you've been a good little developer and not saved their password, but some encrypted variant, you'll have an additional challenge. One way around this is to apply the same encryption to the password the user types, via some more client-side Javascript, as you apply when you save it to the database. For example, if you've saved an MD5 digest of the password to the database rather than the actual password, then you can MD5 what the user types, then run it through the MD5 again to send it up to the server.
Just this tiny extra bit of work to your web application will keep your user's passwords from passing over the internet in clear text, where any nefarious person can grab them and try to exploit them. It's not invulnerable, but it's a lot more secure than no security at all.
Update: I should mention that you can make this method a good deal more secure if you use some salt. Without salt, someone can capture your MD5 hash as it goes by on the wire and run it through a dictionary attack. If you've used a dictionary word, they can figure out your password. This never occurred to me, because I never, ever use dictionary words as passwords. Here's an essay on picking good passwords.
3 comments:
THanks so much your article on Challenge - response authentification. I have one question.In your article you use dojo.crypto. which version of dojo were you using and where can I find the crypto package from dojo? I am using the 1.2 version and cannot find this package anywhere.
Thanks for your help
For the most part, the book is a set of essays against current educational methodologies.
This post is really awesome, It blow up my mind I really appreciate this content thanks for sharing this kind of informative and meaningful post