|
IntroductionThe most common use of the hash scripts is to protect login passwords. Here I explain how do this in a way that provides as much security as possible. Challenge ResponseThe core of the login system is a "challenge response" exchange. The server generates a random number - the challenge - and sends this to the client. The client performs a hash operation including both the challenge and the password, and returns the result to the server. The server checks this against its own calculation. During this exchange, the password is never transmitted as plaintext. Replay attacks are prevented because the challenge is different every time. The hash operation can be based on any algorithm, but this example will use MD5. We could simply use MD5(challenge + password), i.e. just concatonate the strings together. However, there is an alternative technique called HMAC, which involves two regular hash operations and some extra manipulation. This is specifically designed to be secure for such purposes, making it preferable to simple concatonation. This form has two special features:
When the server receives the form data it then performs hex_hmac_md5(password, challenge) using the password it has stored. It then checks the value that the client sent against this. If they match, login is successful. It's important to note that the server must keep track of all the challenges it issues, and only accept those ones for logins. Once a challenge has been used for a login, it must be invalidated. Production systems will also need a mechanism to periodically expire challenges that are issued but never used. Tracking the Login SessionOnce the login has happened, the server must somehow track the user's session. I suggest using a session ID cookie, which is the standard approach these days. With this arrangement, the session cookie can be sniffed and replayed. However the impact of this is much less than the password, because the session ID is time limited. It is theoretically possible to avoid this problem by using challenge-response for every request in a session. However, this would be very complicated to code and even harder to code correctly! I suggest accepting the risk of sniffed session IDs; any sites that can't accept this should probably be using HTTPS. Transfering the Password InitiallyThe challenge response login assumes the server already knows the password. Some other way needs to be found to transfer the initial password. This can be achieved using JavaScript RSA. I'd suggest 512-bit keys, although such numbers have been factored. Protecting the Password on the ServerAn issue with the system described so far is that the server needs to store the plaintext password to check the received hashes. Storing plaintext passwords is risky, considering what could happen if an attacker compromised the database. As users commonly re-use passwords between systems, the attacker could use the passwords to access accounts on other systems. Many corporate password policies forbit storing plaintext passwords. The usual solution to this is password hashing. For example, Unix password files usually use an algorithm based on MD5 to encyrpt passwords. The algorithm includes a number known as a "salt", which is a random number generated for each user. The salt is not a secret. It's purpose is that the same password can now encrypt to many different hashes. This can be applied to challenge response logins with a system like this:
Using this system, the server stores the salt and hex_hmac_md5(password, salt), but does not store the plaintext password. Observant readers may notice that the value the server stores is a "password equivalent", i.e. if you knew just that then you could login. However, the protection is still worthwhile, because its main purpose is to stop an attacker using captured passwords on a different server. Using a different salt for each user presents an issue: the salt isn't known until the user name is known. For a web application, this would require a two-stage login form - one form asking for the user name and a second asking for the password. Such an arrangement would be quite unfriendly towards users. Fortunately, there is a simple alternative. The salt is generated by concatonating the user name with a "system salt". The system salt is the same for all users on one system. TBD: do an example of this Brute Force AttacksOne of the harder attacks to foil is "brute force" attacks, where an attacker tries many different passwords for a user, until they eventually succeed. The use of challenge-response login creates a new risk here. If an attacker captures a login session, they can later conduct an offline brute force attack, where passwords are checked without accessing the server. This bypasses any server restrictions, such as locking an account after three incorrect passwords. The main defence against this is to enforce a secure password policy. If users consistently use long, random passwords then brute force require a large amount of computing power. I suggest using a program like cracklib to check the strength of passwords when users change them - and forbid weak ones. A secondary defence is to increase the computing power an attacker needs to check passwords offline. We can simply repeat the MD5 operation many times, and the attacker will have to do the same for each password. Informal experiments show that doing 100 repeated hashes in JavaScript does not slow things significantly. In Case JavaScript is DisabledJavaScript is not always enabled - either because a cut-down browser doesn't support it, or because the user has chosen to disable it. In this case, encrypted login will not be possible. There are two options for behaviour in this case - either block the login, or allow it to proceed unencrypted. This is a security vs usability tradeoff, different sites will make different choices, although I generally favor allowing the unencrypted login. To permit an unencrypted login, we need to make a slight change to the login form. Add a hidden field "password_hash" and code the JavaScript to populate that with the hash, and blank the password field. If the server receives a login without a password_hash, JavaScript must be disabled. The server checks the password field instead. Blocking unencrypted logins is a little harder. My preffered way is to have JavaScript generate the HTML for the login form, so the form does not appear at all if JavaScript is disabled. An alternative approach is to set the form's target to about:blank, and update it to the correct value in the JavaScript OnSubmit handler. TBD: also possibility that js enabled but hash scripts don't workAlternative SystemI have designed an alternative protocol to challenge-response that solves both the initial password transfer problem and the password storage problem, without needing extra algorithms. The outline of the protocol is as follows. Signup:
Login:
The security of this relies on the non-reversibility property of secure hashes. The client initially sends hex_md5(hex_hmac_md5(password, random1)), then to login has to send hex_hmac_md5(password, random1). An attacker who captures the first value will not be able to reverse the MD5 to generate the second value. Only someone who knows the password can do that. To prevent replay attacks, every time a user logs in a new value is set, based on random2. This scheme has been successfully implemented by Willem Bartels on the live demo of .netjukebox. The protocol appears to be secure against passive attacks. It is completely vulnerable to "man in the middle" attacks, but so is all JavaScript cryptography. It does require a two stage login, although it may be possible to hide that from the user, with some JavaScript trickery. I designed this protocol myself and it has not been subjected to detailed security analysis. Ultimately I consider it interesting enough to have implemented, but I advise against using it on live web sites. More detailed analysis of this protocol yields a couple of interesting facts. The two-stage login is a likely target for "user enumeration" attacks, i.e. an attacker could use it to determine which accounts exist. To stop this, the server must behave identically for accounts that exist and those that do not. This is a little tricky to get right, because the number it returns must be the same every time a particular user name is used, and it must be different for each name. The best solution I have found is for the server to return md5(server_secret + user_name) as the random number, when the account does not exist. This does prevent the leak. However, related to this, an attacker can see when a user has logged in, because the random number changes on every successful login. The change in random number confirms to the attacker that the account does exist. So, there are some information leakages in this protocol - more reasons not to use it in a production environment. Other IdeasFor some websites, saving the challenge every time the login page is requested may be too computationally expensive, e.g. if the login form is shown on every page. An alternative is to have the client generate the challenge. The simplest implementation of this is totally vulnerable to replay attacks, as a malicious client can generate any challenge they like. However, if the server keeps track of used challenges and forbids re-use, then the system is basically secure. On balance I feel this approach is risky, and the used challenges must be forbidden forever. I recommend against implementing this. One suggestion to improve security is to make the client not send the challenge back to the server. Instead the server initially sends (challenge, challenge_id), and the client responds with just the challenge_id. To perform a password guessing attack, an attacker would now have to capture the traffic going in both directions. This does make the task slightly harder, but not much. I don't think the gains are worth coding the functionality specifically. TBD: languages like PHP make doing this easy - recommend it for them? An alternative way has been suggested to implement password changes. Instead of using RSA, a symmetrical algorithm such as Rijndael could be used, with the old password serving as the key. However, I do not recommend this approach. One of the reasons someone may change their password is because the old password has been compromised. With this protocol, an attacker who knows the old password and has sniffed the password change can trivially determine the new password. © 1998 - 2008 Paul Johnston, distributed under the BSD License Updated:15 Dec 2007 |