Introduction
Passwords – Handle them with care! They lock sensitive data behind them, which if broken into, could result in serious issues. Many people use the same password for different sites, which increases the vulnerability of data being misused. Applications with poor security implementations use ineffective password handling and storage mechanisms, which makes matters worse.
As a developer of applications that require some form of user authentication, you probably have to write code for storing/retrieving a user’s password from a data store and verifying it. This article takes you through the different ways how passwords are hacked, lists down the objectives of a secure password system, and finally explores how an effective password handling mechanism can be implemented with.NET.
Overview
In order to design a good password handling solution, we first need to understand how password security is breached. Passwords are usually exposed by one of these mechanisms:
- Brute-force attacks
- Dictionary-attacks
- Security breaches at the data store server
Let’s examine each of these scenarios in detail.
The first two procedures revolve around the idea of guessing passwords by using programs developed specially for that. Brute-force attacks work like this: the target application or the server path is keyed into the brute-force program. The length of the password (if known), and other settings like whether it includes symbols, cases etc. are input. When launched, the program identifies all possible character combinations with the settings selected, and for each word thus generated, it will invoke the application/server link and test whether this password is the right one or not. A simple check whether the response from the server contains a string ‘password failed’ or so will identify the outcome. The more the number of characters and the character range is, the longer the operation will take to complete. In order to decrease the amount of time required most brute-force programs implement multi-threading for executing simultaneous tests.
In many cases, people don’t use strong passwords that involve multiple character casing, numbers, and symbols combined together. Most of the time, passwords are combinations of meaningful words. Dictionary attacks are based on this common vulnerability. Instead of testing all character combinations like in brute-force attacks, dictionary attacks try out common words and their combinations. To facilitate this, huge lists of possible passwords are compiled to what is known as a word-list or a dictionary. With the aid of this, dictionary-attack programs try out each entry in the list to make a break through. Obviously, if the correct password happens to be a weak password, the time taken to figure out the password will be significantly lower when using dictionary attacks instead of brute-force attacks.
Passwords are physically stored in different systems – they may be stored in a database, a simple text file, in the application configuration files etc. In many implementations, the extent of password security depends on how secure the data storage system is. For example, if the data store happens to be a SQL Server database at a web hosting company, the level of password security will depend on how secure the database server at their location is. If the security is not effective enough, it is possible that people who have access to the database can steal passwords.
Basic Security with Hashing
Having explored the different means by which password security is compromised, we could define the following set of objectives, which any good password handling design should satisfy:
- Make sure that brute-force or dictionary attacks will fail in real-life scenarios.
- Make sure that password security does not depend on the security of the data store, i.e. even if security at the data store fails, it should present no risk for the passwords stored.
One solution to fulfill the second objective is, not to store passwords in the data store at all. Instead of storing the passwords as such in their original form; we would store values that are generated from the actual password. The new value sourced from the original password should be in a format with no meaningful data, and should also ensure that all password-related operations could be carried out in the same way as before.
Using cryptography to encrypt the password is the best solution for this requirement. Cryptography will let you transform a piece of meaningful text into non-meaningful data. There are basically three different kinds of cryptography methods:
- Symmetric
- Asymmetric
- Hashing
Symmetric and Asymmetric algorithms both use keys to encrypt and decrypt data. The added overhead of storing the keys securely and the possibility of the passwords being decrypted to their original form makes both these algorithms rather inefficient for our design. Hashing, on the other hand, does not use keys and hashed values cannot be decrypted, and thus turns out to be a good candidate for our design.
Hash algorithms are one-way algorithms, which encrypts data irreversibly. This means that once a piece of text is hashed, the original text can never be retrieved from the hash value. Hashing also ensures that two sets of data with just minor differences will produce hash values that differ from one another significantly. Hence, it would be impossible to infer from any two hash values whether the original text of those hashes was similar or not. Note that with hashing, the same data will always generate the same hash.
In .NET, the most common hashing algorithms used are:
- SHA1 (Secure Hashing Algorithm 1) and,
- MD5 (Message Digest 5).
System.Security.Cryptography.MD5 is one such class that derives from HashAlgorithm. The MD5 class represents the abstract base class from which all MD5 hash algorithm implementations must inherit from. System.Security.Cryptography.MD5CryptoServiceProvider is a class that derives from MD5 and provides the facility to create hashes. In a similar fashion, System.Security.Cryptography.SHA1 and System.Security.Cryptography.SHA1CryptoServiceProvider types are defined for generating hashes using the SHA1 algorithm.To create a hash of a string for storing in a data store, you make use of either the CryptoServiceProvider classes in the System.Security.Cryptography namespace, or the FormsAuthentication.HashPasswordForStoringInConfigFile method. Let’s have a look at how to create a hash for a piece of text using the MD5 algorithm.
The first step in generating a hash is to initialize a cryptographic service provider. In our case, we would initialize an instance of MD5CryptoServiceProvider.
MD5CryptoServiceProvider myMD5 = new MD5CryptoServiceProvider();
The ComputeHash method accepts only an array of bytes or a stream. To convert our string into an array of bytes, we make use of the UnicodeEncoding.UTF8.GetBytes method.
string textToHash = "password"; byte[] byteRepresentation = UnicodeEncoding.UTF8.GetBytes(textToHash);
We generate the hash by passing the ComputeHash method the array of bytes which represents our string to be encrypted.
byte[] hashedTextInBytes = myMD5.ComputeHash(byteRepresentation);
The ComputeHash method returns the encrypted data as an array of bytes, which we need to convert back into a string. We perform this by using the ToBase64String method of the Convert class.
<b">string hashedText = Convert.ToBase64String(hashedTextInBytes);
The complete code will take the form as follows.
public void HashText() { string textToHash = "password"; byte[] byteRepresentation = UnicodeEncoding.UTF8.GetBytes( textToHash); byte[] hashedTextInBytes = null; MD5CryptoServiceProvider myMD5 = new MD5CryptoServiceProvider(); hashedTextInBytes = myMD5.ComputeHash(byteRepresentation); string hashedText = Convert.ToBase64String(hashedTextInBytes); // will display X03MO1qnZdYdgyfeuILPmQ== MessageBox.Show(hashedText); }
In contrast, the FormsAuthentication.HashPasswordForStoringInConfigFile method creates a hash value in a single step. We just need to pass the string to be hashed and the name of the hash algorithm to be used which can be either of MD5 or SHA1. For example:
FormsAuthentication.HashPasswordForStoringInConfigFile("password", "MD5");
Additional Security with Salting
Hashing passwords and storing the hash value in the data store instead of the actual password will ensure that in the event of the web server/data store security being compromised, the user passwords are not at any risk. But sadly, this system will not satisfy our first objective, which is to prevent brute-force or dictionary attacks. Hashing will only ensure that people will not be able to deduce what the actual passwords are from their encrypted values; but it does not prevent people from guessing passwords and testing it with brute-force or dictionary attacks. Moreover, if someone gets access to the users table where the hashed passwords are stored, he/she might be able to find identical hashes – in other words, identical passwords – in multiple user rows. Such scenarios could increase the possibilities of an attack being successful.
To counter this situation, we make use of a concept known as salting. Salting is a method in which, just before generating the hash of a text, we attach a piece of random string – known as a salt – into the original text. Since the salt is generated by random, it makes sure that even if two original texts are the same, the salt always being different, will result in the subsequent hashes to be different. In such an implementation, we will have to store the salt along with the hashed value of the salted password because, while verification we will have to apply the same salt to the user input and then generate the hash. Preferably, the salt should not be stored in the same table as that of the user password so as to increase security.
There are different ways to create a random piece of data that can be used for salting –the most common ones are:
- Creating a random GUID by using the Guid type.
- Creating a random string of digits by using the RNGCryptoServiceProvider class.
To create a new random GUID, we invoke the NewGuid method on the Guid type. Once generated, we simply append the salt to the string to be encrypted.
string saltAsString = Guid.NewGuid().ToString();
For creating a random string of digits by using the RNGCryptoServiceProvider class, we first initialize a provider and a byte array and then invoke the GetBytes method on our provider instance.
byte[] saltInBytes = new byte[8]; RNGCryptoServiceProvider saltGenerator = new RNGCryptoServiceProvider(); saltGenerator.GetBytes(saltInBytes); string saltAsString = Convert.ToBase64String(saltInBytes);
The following code is a modified version of the previous snippet to demonstrate salting.
public void HashText() { string textToHash = "password"; string saltAsString = Guid.NewGuid().ToString(); byte[] byteRepresentation = UnicodeEncoding.UTF8.GetBytes( textToHash + saltAsString); byte[] hashedTextInBytes = null; MD5CryptoServiceProvider myMD5 = new MD5CryptoServiceProvider(); hashedTextInBytes = myMD5.ComputeHash(byteRepresentation); string hashedText = Convert.ToBase64String(hashedTextInBytes); // will display X03MO1qnZdYdgyfeuILPmQ== MessageBox.Show(hashedText); }
Conclusion
Let us revisit and summarize the steps that our new password storage system will involve. We will do so by considering the scenario of a web site where a new user requires to register and user authentication is performed when a user tries to log in.
When a new user registers on our site, our design will execute the following steps:
The new user signs up in our application and selects a user id and a password.
- We create a salt by generating a random piece of data.
- We ‘apply’ the salt to the password.
- We generate the hash for the salted password.
- We store the user id, the hash and the salt in the data store
Now, when a user tries to log in, the following steps will be executed:
- The user enters the user id and the password.
- Our application fetches the hash and the salt of the user id from the data store.
- We apply the salt to the password the user has entered, and then create it’s hash.
- We verify whether this hash is the same as the hash we fetched from the data store.
- If they are the same, the user is authenticated.
Cheers!
About the Author:
The Author of this Article Rakesh Rajan passed away in 2006. He was a student of Benny Alexander, (one of the directors of Macronimous). Rakesh was an MVP in C# and MCSD in .NET. He co-authored more than a book for Microsoft/A Press. We at Macronimous miss him a lot. (Updated August 2017)