The .NET framework contains a number of cryptography services that allow you to incorporate security services into your .NET applications. These libraries are located under the
System.Security.Cryptography namespace and provide various functions such as encryption and decryption of data, as well as other operations such as hashing and random-number generation. In this article, I will show you how to use some of the common security APIs to make your .NET applications more secure.
The first, most common security function that you will perform is hashing. Consider the situation where you need to build a function to authenticate users before they can use your application. You would require the user to supply a login credential, most commonly containing a user name and a password. This login information would need to be persisted to a database. And it is not uncommon for developers to store the passwords of users directly on a database. This is a big security risk, because hackers who had a chance to glance at the users' database would be able to obtain the passwords of your users. A better approach is to store the hash values of the users' passwords, instead of the users' passwords themselves. A hashing algorithm has the following properties:
Instead of storing the passwords of your users verbatim in the database, you should store their hash values. When the user logs in to your application, the password provided is compared with the hash values stored in the database. In this way, even if hackers actually stole the user's database, it does not expose the actual password. One downside to storing the hash values of users' passwords is that in the event that a user loses his password, there is no way of retrieving it. In this case, you'd need to generate a new password for the user and request that he change it immediately. But this inconvenience is a small price to pay for the security of your application.
There are many hashing algorithms available in .NET, but the most commonly used are the SHA1 and MD5 implementations. Let's take a look at how they work in .NET.
Using Visual Studio 2005, create a new Console application project using Visual Basic. Import the following namespaces:
Imports System.Text.Encoding Imports System.Security.Cryptography Imports System.IO Define the following subroutine: Private Sub Hashing_SHA1() '---ask the user to enter a password--- Console.Write("Please enter a password: ") Dim password As String = Console.ReadLine() '---hash the password--- Dim data() As Byte = ASCII.GetBytes(password) Dim passwordHash() As Byte Dim sha As New SHA1CryptoServiceProvider() passwordHash = sha.ComputeHash(data) '---ask the user to enter the same password again--- Console.Write("Please enter password again: ") password = Console.ReadLine() '---hash the second password and compare it with the first--- data = System.Text.Encoding.ASCII.GetBytes(password) If ASCII.GetString(passwordHash) = _ ASCII.GetString(sha.ComputeHash(data)) Then Console.WriteLine("Same password") Else Console.WriteLine("Incorrect password") End If End Sub
In this subroutine, you first ask the user to enter a password, after which you will hash it using the SHA1 implementation. You then ask the user to enter the same password again. To verify whether the second password matches the first, you hash the second password and then compare the two hash values. Note that for the SHA1 implementation, the hash value generated is 160 bits in length (the byte array
passwordHash has 20 members [8 bits x 20 = 160 bits]). In my example, I converted the hash values into strings and performed a comparison. You could also convert them to Base64 encoding and then perform a comparison. Alternatively, you can also compare the two hash values using their byte arrays, comparing byte by byte. As soon as one byte is different, you can conclude that the two hash values are not the same.
To test the subroutine, simply call the
Hashing_SHA1() subroutine in
Sub Main() Hashing_SHA1() Console.Read() End Sub
Figure 1 shows the subroutine in action.
Figure 1. Using Hashing for authentication
You can also use the MD5 implementation to perform hashing, as the following subroutine shows:
Private Sub Hashing_MD5() '---ask the user to enter a password--- Console.Write("Please enter a password: ") Dim password As String = Console.ReadLine() '---hash the password--- Dim data() As Byte = ASCII.GetBytes(password) Dim passwordHash() As Byte Dim md5 As New MD5CryptoServiceProvider() passwordHash = md5.ComputeHash(data) '---ask the user to enter the same password again--- Console.Write("Please enter password again: ") password = Console.ReadLine() '---hash the second password and compare it with the first--- data = ASCII.GetBytes(password) If ASCII.GetString(passwordHash) = _ ASCII.GetString(md5.ComputeHash(data)) Then Console.WriteLine("Same password") Else Console.WriteLine("Incorrect password") End If End Sub
The main difference is that the hash value for MD5 is 128 bits in length.
With hashing, you simply store the hash value of a user's password in the database. However, if two users have the same password, then the hash values for these two passwords would be identical. Imagine the hacker seeing that the two hash values are identical; it would not be hard for him to guess that the two passwords must be the same. For example, users often like to use their own names (or birth dates, or common words found in the dictionary) as passwords. Hence, hackers often like to use dictionary attacks to correctly guess users' passwords. To reduce the chance of dictionary attacks, you can add a "salt" to the hashing process so that no two identical passwords can generate the same hash values. For example, instead of hashing a user's password, you can hash his password together with his other information, such as email address, birth date, last name, first name, etc. The idea is to ensure that each user will have a unique password hash value. While this idea of using the user's information as a salt for the hashing process sounds good, it is quite easy for hackers to guess. A better approach would be to randomly generate a number to be used as the salt and then hash it together with the user's password.
The following subroutine,
Salted_Hashing_SHA1(), generates a random number using the
RNGCryptoServiceProvider class, which returns a list of randomly generated bytes (the salt). It then combines the salt with the original password and performs a hash on it.
Private Sub Salted_Hashing_SHA1() '---Random Number Generator--- Dim salt(8) As Byte Dim rng As New RNGCryptoServiceProvider rng.GetBytes(salt) '---ask the user to enter a password--- Console.Write("Please enter a password: ") Dim password As String = Console.ReadLine() '---add the salt to the password--- password &= ASCII.GetString(salt) '---hash the password--- Dim data() As Byte = ASCII.GetBytes(password) Dim passwordHash() As Byte Dim sha As New SHA1CryptoServiceProvider() passwordHash = sha.ComputeHash(data) '---ask the user to enter the same password again--- Console.Write("Please enter password again: ") password = Console.ReadLine() Console.WriteLine(ASCII.GetString(salt)) '---adding the salt to the second password--- password &= ASCII.GetString(salt) '---hash the second password and compare it with the first--- data = ASCII.GetBytes(password) If ASCII.GetString(passwordHash) = _ ASCII.GetString(sha.ComputeHash(data)) Then Console.WriteLine("Same password") Else Console.WriteLine("Incorrect password") End If End Sub
Note that if you use salted hashing for storing passwords, the "salt" used for each password must be stored separately from the main hash database so that hackers do not have a chance to obtain it easily.
In the previous section, you saw how hashing works. Hashing is a one-way process, which means that once a value is hashed, you can't obtain its original value by reversing the process. This characteristic is particularly well suited for authentications as well as digitally signing a document.
In reality, there are many situations that require information to be performed in a two-way process. For example, if you send a secret message to a recipient, you would need to be able to "scramble" it so that only the recipient can see it. This process of scrambling is known as encryption. Undoing the scrambling process to obtain the original message is known as decryption. There are two main types of encryption, symmetric and asymmetric.
Symmetric encryption is also sometimes known as private key encryption. With private key encryption, you encrypt a secret message using a key that only you know. To decrypt the message, you need to use the same key. Private key encryption is effective only if the key can be kept a secret. If too many people know the key, its effectiveness is reduced.
Imagine you are trying to send a secret message to your faraway friend, Susan, using a private key. In order for Susan to decrypt the secret message, she must know the private key. So you need to send it to her. But if the secrecy of the key is compromised somehow (such as people eavesdropping on your conversation), then the message is no longer secure. Moreover, if Susan tells another friend about the private key, her friend can then also decrypt the message. Despite the potential weakness of private key encryption, it is very easy to implement and, computationally, it does not take up too many resources.
For private key encryption (symmetric) encryptions, the .NET framework supports the DES, RC2, Rijndael, and TripleDES algorithms.
To demonstrate symmetric encryption, I will use the
RijndaelManaged class in the following
SymmetricEncryption() function. Three parameters are required--the string to be encrypted, the private key, and the initialization vector (IV). The IV is a random number used in the encryption process to ensure that no two strings will give the same cipher text (the encrypted text) after the encryption process. You will need the same IV later on when decrypting the cipher text.
Private Function SymmetricEncryption( _ ByVal str As String, _ ByVal key As Byte(), _ ByVal IV As Byte()) As String Dim memStream As New IO.MemoryStream Try '---creates a new instance of the RijndaelManaged class--- Dim RMCrypto As New RijndaelManaged '---creates a new instance of the CryptoStream class--- Dim CryptStream As New CryptoStream(memStream, _ RMCrypto.CreateEncryptor(key, IV), _ CryptoStreamMode.Write) Dim SWriter As New StreamWriter(CryptStream) '---encrypting the string--- SWriter.Write(str) SWriter.Close() CryptStream.Close() '---return the encrypted data as a string--- Return System.Convert.ToBase64String(memStream.ToArray) Catch err As Exception Console.WriteLine(err.ToString) Return (String.Empty) End Try End Function
In the previous function, the encrypted string is returned as a Base64-encoded string. Note the allowable key sizes for the
RijndaelManaged class. You can check the allowable key sizes using the following code:
Dim ks() As KeySizes Dim RMCrypto As New RijndaelManaged ks = RMCrypto.LegalKeySizes '---print out the various key sizes--- Console.WriteLine(ks(0).MaxSize) ' 256 Console.WriteLine(ks(0).MinSize) ' 128 Console.WriteLine(ks(0).SkipSize) ' 64
The valid key sizes are: 16 bytes (128 bit), 24 bytes (128 bits + 64 bits), and 32 bytes (256 bits).
Also, you can get the system to generate a random key and IV (which you need to supply in the current example) automatically:
'---generate key--- RMCrypto.GenerateKey() Dim key As Byte() = RMCrypto.Key Console.WriteLine("Key : " & System.Convert.ToBase64String(key)) '---generate IV--- RMCrypto.GenerateIV() Dim IV As Byte() = RMCrypto.IV Console.WriteLine("IV : " & System.Convert.ToBase64String(IV))
If the IV is null when it is used, the
GenerateIV() method is called automatically. Valid size for the IV is 16 bytes.
To decrypt a string encrypted using the
RijndaelManaged class, you can use the following
Private Function SymmetricDecryption( _ ByVal str As String, _ ByVal key As Byte(), _ ByVal IV As Byte()) _ As String Try Dim s As String '---converts the encrypted string into a byte array--- Dim b As Byte() = System.Convert.FromBase64String(str) '---converts the byte array into a memory stream for ' decryption--- Dim memStream As New MemoryStream(b) Dim RMCrypto As New RijndaelManaged Dim CryptStream As New CryptoStream(memStream, _ RMCrypto.CreateDecryptor(key, IV), _ CryptoStreamMode.Read) '---decrypting the stream--- Dim SReader As New StreamReader(CryptStream) s = SReader.ReadToEnd '---converts the decrypted stream into a string--- s.ToString() SReader.Close() Return s Catch err As Exception Console.WriteLine(err.ToString) Return String.Empty End Try End Function
The following code snippet shows how to use the
SymmetricDecryption() functions to encrypt and decrypt a string:
'---encrypt the string--- Dim cipherText As String = _ SymmetricEncryption("This is a string", key, IV) Console.WriteLine("Ciphertext: " & cipherText) '---decrypt the string--- Console.WriteLine("Original string: " & _ SymmetricDecryption(cipherText, key, IV))
Figure 2 shows the output.
Figure 2. Symmetric encryption and decryption in action
Private key encryption requires the key used in the encryption process to be kept a secret. A more effective way to transport secret messages to your intended recipient is to use asymmetric encryption (also known as public key encryption). In public key encryption, there is a pair of keys involved. This pair, consisting of a private and a public key, is related mathematically such that messages encrypted with the public key can only be decrypted with the corresponding private key. The contrary is true; messages encrypted with the private key can only be decrypted with the public key. Let's see an example for each scenario.
Before you send a message to Susan, Susan needs to generate the key pair containing the private key and the public key. Susan then freely distributes the public key to you (and all her other friends) but keeps the private key to herself. When you want to send a message to Susan, you use her public key to encrypt the message and then send it to her. Upon receiving the encrypted message, Susan proceeds to decrypt it with her private key. In this case, Susan is the only one who can decrypt the message, since the key pair works in such a way that only messages encrypted with the public key can be decrypted with the private key. Also, there is no need to exchange secret keys, thus eliminating the risk of compromising the secrecy of the key.
The reverse can happen. Suppose Susan now sends a message encrypted with her private key to you. To decrypt the message, you need the public key. The scenario may seem redundant, since the public key is not a secret; everyone knows it. But using this method guarantees that the message has not been tampered with and it indeed comes from Susan. If the message had been modified, you would not be able to decrypt it. The fact that you can decrypt the message using the public key proves that the message has not been modified.
In computing, public key cryptography is a secure way to encrypt information. However, it is computationally expensive, as it is time-consuming to generate the key pairs and to perform encryption and decryption. It is usually used for encrypting a small amount of sensitive information.
For public key (asymmetric) encryptions, the .NET framework supports the DSA and RSA algorithms. To demonstrate asymmetric encryption, I will use the RSA algorithm. To do so, I have created the following
AsymmetricEncryption() function. This function takes in two parameters: the string to be encrypted and the public key:
Private Function AsymmetricEncryption( _ ByVal str As String, _ ByVal publicKey As String) _ As String '---Creates a new instance of RSACryptoServiceProvider--- Try Dim RSA As New RSACryptoServiceProvider '---Loads the public key--- RSA.FromXmlString(publicKey) Dim EncryptedStr() As Byte '---Encrypts the string--- EncryptedStr = RSA.Encrypt(ASCII.GetBytes(str), _ False) '---Converts the encrypted byte array to string--- Return System.Convert.ToBase64String(EncryptedStr) Catch err As Exception Console.WriteLine(err.ToString) Return String.Empty End Try End Function
The encrypted string is returned in the Base64 encoding. To decrypt a string encrypted with the public key, define the following
AsymmetricDecryption() function. This function takes in two parameters: the encrypted string and the private key. It returns the decrypted string.
Private Function AsymmetricDecryption( _ ByVal str As String, _ ByVal privateKey As String) As String Try '---Creates a new instance of RSACryptoServiceProvider--- Dim RSA As New RSACryptoServiceProvider '---Loads the private key--- RSA.FromXmlString(privateKey) '---Decrypts the string--- Dim DecryptedStr As Byte() = _ RSA.Decrypt(System.Convert.FromBase64String(str), False) '---Converts the decrypted byte array to string--- Return ASCII.GetString(DecryptedStr) Catch err As Exception Console.WriteLine(err.ToString) Return String.Empty End Try End Function
The following code snippet shows how to use the
AsymmetricDecryption() functions to encrypt and decrypt a string:
Dim publicKey, privateKey As String Dim RSA As New RSACryptoServiceProvider() '---get public key--- publicKey = RSA.ToXmlString(False) Console.WriteLine("Public key: " & publicKey) Console.WriteLine() '---get private and public key--- privateKey = RSA.ToXmlString(True) Console.WriteLine("Private key: " & privateKey) Console.WriteLine() '---encrypt the string--- Dim cipherText As String = _ AsymmetricEncryption("This is a string", publicKey) Console.WriteLine("Ciphertext: " & cipherText) Console.WriteLine() '---decrypt the string--- Console.WriteLine("Original string: " & _ AsymmetricDecryption(cipherText, privateKey)) Console.WriteLine()
You can obtain the public and private keys generated by the RSA algorithm by using the
ToXmlString() method from the
RSACryptoServiceProvider class. This method takes in a Boolean variable, and returns a public key if the value
False is supplied. If the value
True is supplied, it returns both the private and public keys.
Figure 3 shows the output.
Figure 3. Asymmetric encryption and decryption in action
In this article, I have covered some of the common functions that you will perform when you are securing your .NET applications. As the .NET framework provides implementation for all the major cryptography algorithms, there is no reason why you should not spend some time incorporating these security features into your .NET applications.
Wei-Meng Lee (Microsoft MVP) http://weimenglee.blogspot.com is a technologist and founder of Developer Learning Solutions http://www.developerlearningsolutions.com, a technology company specializing in hands-on training on the latest Microsoft technologies.
Return to Windows DevCenter.
Copyright © 2009 O'Reilly Media, Inc.