Cryptography has always been an endless loop wherein coders try to hide valuable information using newer and more powerful techniques, and decoders, in turn, devise creative and ingenious hacks to get at that information. Most websites -- and especially large social networks -- are tasty morsels to attackers. For more information on ASP.NET security, see "Securing ASP.NET MVC" and "The Definitive Guide to ASP.NET Security."
In recent months, we've read about how hackers breached the LinkedIn website and picked up many users' passwords. After that happened, I suspect that many of us got a call from our bosses seeking reassurance that our organization's website was secure. We all wish that securing passwords were a simple matter of storing them in a server-side database as plain text, but in reality, doing so is a very bad practice. Even so, I'm sure that many websites still employ a user account database table where the password column contains plain strings. And I'm sure that owners of these websites are blissfully happy and haven't experienced any security breaches -- yet.
To guard your website users' passwords, you need to find a way to hide them from potential attackers -- but how? The obvious answer is to store a unique fingerprint of the password instead of the password itself -- that is, a hash value. But this is precisely what people at LinkedIn reportedly did; thus, plain hashing might not be enough to adequately secure passwords, especially when your site has some value to attackers.
The best approach to password security uses a combination of tactics: a strong hashing strategy with database and connection encryption and policies such as changing passwords periodically, locking down access after a minimal number of attempts, enforcing a minimum character length, and requiring passwords to contain a mix of numbers, alphabetic characters (lowercase and uppercase), and symbols. In this article, I'll focus on hashing strategies you can use as part of your ASP.NET website security approach.
Why Hashing Alone Isn't Enough
Certainly, hashing your users' passwords is preferable to not hashing them, but if you're really looking for stronger security, hashing alone isn't enough. I even dare say that once your site becomes tempting to hackers -- and hackers typically have plenty of processing power to work with -- there's little difference between storing plain passwords or plain hashes of passwords.
Hashing consists of applying a transformation function to a string so that distinct strings are never mapped to the same output string. Moreover, a hashing function is expected to show no relationship between closeness of input strings and closeness of output strings. If the input string is modified by a single character, then the output string is expected to be completely different.
When a user creates an account and enters a username and password, your membership system will compute the hash of the password and store that string instead of the plain password in the database. When the user attempts to log on and types the password, the hash of the entered password is computed in memory and matched to the stored hash read from the database. Although this would appear to be an effective and overall secure method, it isn't really so, as the LinkedIn incident reminds us.
The point is that there are a few ways for hackers to guess passwords from hashes. Nastily enough, some of these techniques can even work quickly. Hackers can use basic techniques such as dictionary or even brute-force attacks. In this case, they pick up passwords from a known dictionary of common passwords (or generate them iteratively up to a given size and alphabet), hash the passwords, and make an attempt against your membership system.
Note that setting a minimal number of unsuccessful attempts might help stop or slow these attacks -- but not if hackers exploiting other holes in your system can gain direct access to your database and possibly copy it. Thankfully, dictionary or brute-force attacks are slow and require significant resources; however, in theory there's no way for any system to resist a brute-force attack on each hash, no matter how smart the hashing system might be. So for developers, password security is all about implementing techniques to mitigate the effectiveness of attacks; for hackers, it's all about finding ways to examine as many hashes as possible per second.
A popular hacking technique known as lookup tables entails the use of ad hoc tables of pre-computed hashes for known passwords. An even smarter technique uses a variation of lookup tables called rainbow tables. What makes rainbow and lookup tables especially effective is just the inherent behavior of hash functions: Two users using the same password end up having the same hash. This might significantly reduce the time needed to guess passwords, especially for weak passwords that are easily guessed.
For this reason, you can prepend (or append) a random (and user-specific) string to the password. This string -- known as the salt -- doesn't have to be a secret and must be stored in the database along with the hash. The benefit of the salt is that two users can then use the same password, but doing so results in completely different hashes. Thus, using salted hashes is a must if you're serious about storing passwords.
Hashing with the Default ASP.NET Membership Provider
You don't have to write hash functions yourself; writing hash functions is a serious task and requires advanced math skills. Most frameworks -- notably the .NET Framework -- provide built-in classes that implement hash functions. So at the end of the day, all you need to do is pick up one of the few built-in hash classes and go.
If you use the membership system of ASP.NET and the default SQL Server membership provider, then hashing capabilities are purely a matter of configuration. Simply setting the passwordFormat attribute of the provider configuration section, as shown in the following example, will cause users' passwords to be hashed with a random salt value, which will make them even more difficult to guess. (I'll discuss salt more in a moment.)
- <add type="..." passwordFormat="Hashed" ... />
Until ASP.NET 4, the default hashing algorithm was the Secure Hash Algorithm 1 (SHA-1). You can change the default hashing algorithm used throughout the ASP.NET platform with the following setting:
- <add type="..." passwordFormat="Hashed" hashAlgorithmType="SHA1" ... />
Over the years, a few issues have been found with the SHA-1 algorithm, so that currently SHA-256 is considered a good compromise between speed and security. .NET provides an implementation of the SHA-256 algorithm via the SHA256Managed class.
Manual Settings on Custom Providers
As you can see, choosing a hash algorithm (and using salt) is easy as long as you use the default SQL Server membership provider. Many developers, though, use a custom membership provider. In this case, chances are you'll need to manage hashing manually through the classes provided by the .NET Framework. The aforementioned SHA256Managed class defined in the System.Security.Cryptography namespace can do a good job for you in this regard. Figure 1 shows how you can obtain a hashed password using this class.
- public String HashPassword(String password, String salt)
- var combinedPassword = String.Concat(password, salt);
- var sha256 = new SHA256Managed();
- var bytes = UTF8Encoding.UTF8.GetBytes(combinedPassword);
- var hash = sha256.ComputeHash(bytes);
- return Convert.ToBase64String(hash);
The salt is essentially a randomly generated number of a given size. The size of the salt string is a crucial factor: A too-short salt would make a rainbow table attack only a bit slower. The minimum password length is considered a good size for the salt. For example, if 12 characters is the minimum password length, then a random string of 12 characters is a good salt. How do you generate the salt? There's just one rule: Use a cryptographically strong random generator and avoid using the Random class. The difference is all in the internal algorithm used to generate the random number. Simply put, Random uses a calculation and isn't guaranteed to be unpredictable; in this regard, a strong generator offers much better warranties. In .NET, you use RNGCryptoServiceProvider to get a really unpredictable random number. Figure 2 shows you how to use it to get a salt string.
- public String GetRandomSalt(Int32 size = 12)
- var random = new RNGCryptoServiceProvider();
- var salt = new Byte[size];
- return Convert.ToBase64String(salt);
Once you've obtained a salt and a hash, you store them in the database. The code in Figure 3 shows what's required to validate the password entered by the user.
- public Boolean ValidatePassword(String enteredPassword, String storedHash, String storedSalt)
- // Consider this function as an internal function where parameters like
- // storedHash and storedSalt are read from the database and then passed.
- var hash = HashPassword(enteredPassword, storedSalt);
- return String.Equals(storedHash, hash);
Note that a salt string's effectiveness is strongly related to its unpredictability. Using the same salt string for all users makes the trick nearly useless. Ideally, you should be using a different salt string for each user and each password. This means that every time a user changes his or her password (or the password is reset by the system), a new salt string should be generated and used. This leads to the need to tweak other aspects of a membership provider, such as the way in which a password is reset. In particular, letting users reset their passwords can be risky; it's preferable to do so via email by sending users a link to click. An attacker might have grabbed an authentication token for a given user, but cracking the user's email Inbox folder is another thing. In addition, the link to reset the password shouldn't contain any information that might suggest how to use it to reset another user's password. The best method is using a globally unique identifier (GUID) that's associated with a unique account and that expires immediately after use.
Hashing vs. Encryption
The default ASP.NET membership provider supports both hashed and encrypted passwords. You choose to use encryption instead of hashing by setting the passwordFormat attribute to Encrypted. Apparently, encryption is a much stronger way to deal with password security, yet hashing is the standard way to go. What's the difference between the two methods?
Hashing and encryption are both based on functions that transform some input into some output. The primary difference is that hashing isn't invertible. With hashing, once you have the output -- the hash value -- in no way can you turn it back into the original input -- the password. All an attacker can do to grab the password is run some sort of dictionary attack and hope. However, you do want an encryption function to be invertible; otherwise, how can you get your secret data back?
Overall, hashing and encryption exist to serve different use cases. As far as passwords are concerned, hashing is a much better choice because there are no keys to keep hidden. It's a quick mathematical calculation to get a hash and check it, and the secret data -- the password -- isn't stored anywhere and remains only in the user's memory. If an encrypted source is violated (i.e., the key is found), all the content is accessible. If a hashed source is violated (i.e., the database is cracked), the attacker still needs to run a dictionary attack to grab passwords.
Even Stronger Hashing
Although using salt strings makes it significantly more difficult for attackers to find passwords, you can raise the security bar even higher by iterating the hash and adding salt at each step. Simply put, you start from the password and run the hash function in a loop of hundreds or even thousands of steps. Each step will concatenate the hash with the original password and salt. This process makes running the hash slower and then makes it more difficult to sustain in case of brute-force attack; in addition, it calculates the hash on a larger string, reducing the already remote risk of hash collisions. This technique is known as key-stretching and is implemented in the .NET Framework via the Rfc2898DeriveBytes class in System.Security.Cryptography. Note, though, that the number of iterations is crucial: A high number (in the thousands) makes the hash stronger but might pose performance issues on websites during authentication. You can learn more about this topic in the MSDN article "Password Minder Internals."
Step Up Your Password Protection
As the LinkedIn breach proved, poor and unsalted hashing offers little protection from password hackers. If you want to protect your passwords even more, employ a key-stretching algorithm. And by the way, be aware that any change of this type does have an effect on the existing database. You can't change the hashing algorithm and maintain the same stored hash values. Therefore, a well-defined plan is a must.
Dino Esposito is a trainer and consultant specializing in web, social, and mobile integrated architecture and effective code design principles and practices. He's the author of Programming Microsoft ASP.NET 4 and Programming Microsoft ASP.NET MVC3 (Microsoft Press).