Introduction
Using the built in crypto classes in .NET can be surprisingly complicated. You need a lot of understanding of what you are doing to get it right and how to work in a secure manner. The choice of algorithm, what cipher mode, key length, block size and understand what salt is and how to use it and also how to hash a password to a proper key are some things you need to deal with. Hopefully, this article will make your life a lot easier.
For those who are looking for a quick solution this is the basically the code you need to encrypt and decrypt data. And there is a lot of other examples in the downloadable code.
string enc = CryptoHelper.EncryptBase64String("password", AutoSaltSizes.Salt512, "data");
string dec = CryptoHelper.DecryptBase64String("password", AutoSaltSizes.Salt512, enc);
My implementation is a class library with useful functions to do one liner encryption which will help you with the most common scenarios. I have also added something I call AutoSalt which is a way to secure your data further with no extra effort from your end other than deciding how big the salt value should be. Furthermore, I have done my own implementation of what CryptoStream
is doing to get some extra control on what is going on in the stream which will help with the more complex scenarios.
Background
The reason I started this article is because I started looking into encryption with CryptoStream
and it was useless for what I needed to achieve in a project. The reason is that the CryptoStream
is limited to push everything through a stream and there is no way you can pick up the data while it's being encrypted. I guess this part is a bit specialized for my needs but I don't have a doubt that there are more people out there with similar issues.
A simple example of what you can't do with CryptoStream
that I can easily handle with my implementation is to split a 50GB file into a series of smaller files with 100MB data each in one go. You may have seen .rar files split up like this, it's kind of the same idea.
This part of my project then evolved to writing a full library and then I just felt like sharing with this article.
Why Use this Lib
For me, security has been a real concern and a growing interest when coming up with this library. Because encryption is not just down to the algorithm, it's also about how you store keys generate keys/Salt in a secure manner, making sure that your application has a safe flow in how the data is handled, and also making sure you made the right choices of how to set up your encryption in a good way. Even performance could play an important part if you are dealing with huge sets of data.
Obviously, I am not solving all of these problems but some of them, it will definetly make your life easier so you can focus on the quality of your code. Here is a few things I want to highlight
- I have added a feature called AutoSalt which creates cryptographically safe random data and combines this with the data. So if you encrypt the same thing twice, the encrypted data will look different.
- I have made sure you can utilize hardware acceleration if supported by your hardware.
- I generate key and Initvector in a good secure way so you don't have to bother about it.
- You still have the the flexibility to use all available crypto settings in .NET
- For simple scenarios, the provided helper class will give you one liners to use the encryption in a simple way.
- For advanced users, you will be able to cut in and read from the encrypted/decrypted data stream before the entire process is complete.
So anyone that just wants to encrypt something simple or has issues using CryptoStream
because it does not do what you want it to do should use this library instead.
Using the Code
This library is built for being simple to use but with flexibility and security in mind. Therefore, you have access to the main classes to be able to do more advanced things and the helper classes that will help you in doing oneliners.
You will be able to choose one of the following symmetric algorithms, key size and cipher mode and block size when you work with encryption. I default to AES with 256bit key and CBC cipher mode because it's the current standard and if your CPU is not too old, CPU hardware acceleration will be available which is always nice.
The combinations from the above grid are expressed as options in the following Enum
. It is formatted like this: [Symmetric Algorithm]_[Key Size]_[Cipher Mode]_[Block Size]. I choose to create an Enum
like this because there are so few combinations shared by the algorithms and it's also nice to never allow you to give an incorrect option. The block size is only available for Rijndael because it is the only symmetric algorithm that supports it.
public enum SymmetricCryptoAlgorithm
{
AES_128_CBC,
AES_128_ECB,
AES_128_CFB,
AES_192_CBC,
AES_192_ECB,
AES_256_CBC,
AES_256_ECB,
AES_256_CFB,
Rijndael_128_CBC_128,
Rijndael_128_CBC_192,
Rijndael_128_CBC_256,
Rijndael_128_ECB_128,
Rijndael_128_ECB_192,
Rijndael_128_ECB_256,
Rijndael_128_CFB_128,
Rijndael_128_CFB_192,
Rijndael_128_CFB_256,
Rijndael_192_CBC_128,
Rijndael_192_CBC_192,
Rijndael_192_CBC_256,
Rijndael_192_ECB_128,
Rijndael_192_ECB_192,
Rijndael_192_ECB_256,
Rijndael_192_CFB_128,
Rijndael_192_CFB_192,
Rijndael_192_CFB_256,
Rijndael_256_CBC_128,
Rijndael_256_CBC_192,
Rijndael_256_CBC_256,
Rijndael_256_ECB_128,
Rijndael_256_ECB_192,
Rijndael_256_ECB_256,
Rijndael_256_CFB_128,
Rijndael_256_CFB_192,
Rijndael_256_CFB_256,
DES_64_CBC,
DES_64_ECB,
DES_64_CFB,
TripleDES_128_CBC,
TripleDES_128_ECB,
TripleDES_128_CFB,
TripleDES_192_CBC,
TripleDES_192_ECB,
TripleDES_192_CFB,
RC2_40_CBC,
RC2_40_ECB,
RC2_40_CFB,
RC2_48_CBC,
RC2_48_ECB,
RC2_48_CFB,
RC2_56_CBC,
RC2_56_ECB,
RC2_56_CFB,
RC2_64_CBC,
RC2_64_ECB,
RC2_64_CFB,
RC2_72_CBC,
RC2_72_ECB,
RC2_72_CFB,
RC2_80_CBC,
RC2_80_ECB,
RC2_80_CFB,
RC2_88_CBC,
RC2_88_ECB,
RC2_88_CFB,
RC2_96_CBC,
RC2_96_ECB,
RC2_96_CFB,
RC2_104_CBC,
RC2_104_ECB,
RC2_104_CFB,
RC2_112_CBC,
RC2_112_ECB,
RC2_112_CFB,
RC2_120_CBC,
RC2_120_ECB,
RC2_120_CFB,
RC2_128_CBC,
RC2_128_ECB,
RC2_128_CFB
}
You also get the option of selecting Padding mode, I picked PKCS7 as the default padding since it seems to be that way for
public enum PaddingMode
{
None,
PKCS7,
Zeros,
ANSIX923,
ISO10126
}
In the example code I have provided, you will be able to see a comparison of performance between all different setups which I think will help you decide what to use depending on what you use it for. I also try to illustrate the different ways of working.
This is an example using my helper class which will help you do one liners for encrypting or decrypting data.
string key = "My super secret password";
string salt = "Salt value";
string dataString = "My secret data";
byte[] dataBytes = Encoding.Unicode.GetBytes(dataString);
var encStr = CryptoHelper.EncryptString
(password, salt, dataString, SymmetricCryptoAlgorithm.TripleDES_192_CBC);
var decStr = CryptoHelper.DecryptString
(password, salt, encStr, SymmetricCryptoAlgorithm.TripleDES_192_CBC);
var encryptedStringAutoSalt =
CryptoHelper.EncryptString(password, AutoSaltSizes.Salt32, dataString);
var decryptedStringAutoSalt =
CryptoHelper.DecryptString(password, AutoSaltSizes.Salt32, encryptedStringAutoSalt);
var encryptedBytes = CryptoHelper.EncryptData(password, salt, dataBytes);
var decryptedBytes = CryptoHelper.DecryptData(password, salt, encryptedBytes);
var encryptedBytesAutoSalt = CryptoHelper.EncryptData
(password, AutoSaltSizes.Salt64, dataBytes);
var decryptedBytesAutoSalt = CryptoHelper.DecryptData
(password, AutoSaltSizes.Salt64, encryptedBytesAutoSalt);
CryptoHelper.EncryptFile(password, salt, "testdata.txt", "testdata encrypted.txt");
CryptoHelper.DecryptFile(password, salt, "testdata encrypted.txt", "testdata decrypted.txt");
CryptoHelper.EncryptFile(password, AutoSaltSizes.Salt128,
"testdata.txt", "testdata encrypted.txt");
CryptoHelper.DecryptFile(password, AutoSaltSizes.Salt128,
"testdata encrypted.txt", "testdata decrypted.txt");
The following example shows how to use the main classes in the simplest scenario.
Important detail: Because of the nature of how the encryption classes work, it wants to know when I'm done entering data into the buffer, it needs to know this so that it can add padding to the data. To handle this in my code, you need to set the lastData
flag to true
when all data is added in the AddData
function.
string dataStr = "My super secret data that I want to encrypt";
byte[] bytes = Encoding.Unicode.GetBytes(dataStr);
EncryptionBuffer encBuffer = new EncryptionBuffer("My super secret password",
"Salt value", SymmetricCryptoAlgorithm.AES_256_CBC);
encBuffer.AddData(bytes, true);
byte[] encryptedBytes = encBuffer.GetData();
string encryptedString = Encoding.Unicode.GetString(encryptedBytes);
DecryptionBuffer decBuffer = new DecryptionBuffer("My super secret password",
"Salt value", SymmetricCryptoAlgorithm.AES_256_CBC);
decBuffer.AddData(encryptedBytes, true);
byte[] decryptedBytes = decBuffer.GetData();
string decryptedString = Encoding.Unicode.GetString(decryptedBytes);
Here is a bit more advanced example where I read a file in chunks and encrypt the chunks and then save it down to another file, and then back again. This example can be easily modified to do whatever you like it to do. It should be thread-safe so that you can read and write data at the same time from two different threads if that is needed.
using (FileStream originalFile = new FileStream("testdata.txt", FileMode.Open, FileAccess.Read))
{
using (FileStream encryptedFile =
new FileStream("testdata.enc", FileMode.OpenOrCreate, FileAccess.Write))
{
EncryptionBuffer encBuffer =
new EncryptionBuffer("My super secret password", "Salt value");
byte[] fileData = new byte[10000];
bool isLastData = false;
while (!isLastData)
{
int nrOfBytes = originalFile.Read(fileData, 0, fileData.Length);
isLastData = (nrOfBytes == 0);
encBuffer.AddData(fileData, 0, nrOfBytes, isLastData);
byte[] encryptedData = encBuffer.GetData();
encryptedFile.Write(encryptedData, 0, encryptedData.Length);
}
}
}
using (FileStream encryptedFile =
new FileStream("testdata.enc", FileMode.Open, FileAccess.Read))
{
using (FileStream decryptedFile =
new FileStream("testdata.dec", FileMode.OpenOrCreate, FileAccess.Write))
{
DecryptionBuffer decBuffer =
new DecryptionBuffer("My super secret password", "Salt value");
byte[] fileData = new byte[10000];
bool isLastData = false;
while (!isLastData)
{
int nrOfBytes = encryptedFile.Read(fileData, 0, fileData.Length);
isLastData = (nrOfBytes == 0);
decBuffer.AddData(fileData, 0, nrOfBytes, isLastData);
byte[] decryptedData = decBuffer.GetData();
decryptedFile.Write(decryptedData, 0, decryptedData.Length);
}
}
}
AutoSalt and Why Use It
Using Salt is most commonly associated with storing password hashes and not so much with encrypting data. But obviously, it can be used for storing encrypted data as well. Salt is actually supposed to be a public key made out of 100% random data. It should also be stored together with the encrypted data.
The reason for using salt is to further obfuscate the data, mostly to hide patterns. If you just encrypt the same data multiple times, the encrypted result will look exactly the same. Introducing a proper salt to this data will make the result look different every time.
What I am doing here with my AutoSalt is generating a random salt value with a length you can set, the salt key will then be concatenated to the encrypted data. So if you choose to encrypt your data with a AutoSaltSizes.Salt256
then 32 bytes of random data will be added to your encrypted data.
public enum AutoSaltSizes:int
{
Salt32 = 4,
Salt64 = 8,
Salt128 = 16,
Salt192 = 24,
Salt256 = 32,
Salt384 = 48,
Salt512 = 64
}
Here is an example of how to use the code with AutoSalt. It's so easy to use and together with the added security value, you should probably never consider not using it. Dealing with saltvalues yourself will only give you more work.
string dataString = "My secret data";
string enc = CryptoHelper.EncryptString("password", AutoSaltSizes.Salt64, dataString);
string dec = CryptoHelper.DecryptString("password", AutoSaltSizes.Salt64, enc);
Here is an extract from my example code demonstrating the result of encrypting the same data with different outputs.
Performance
Here is a comparison between the different crypto algorithms encrypting/decrypting exactly the same data on an Intel i7-3770 CPU. The way you read the labels in the x axis is ALGORITHM_KEYSIZE_CIPHERMODE_BLOCKSIZE, the blocksize part is only there for Rijndael because it's the only algorithm with the option to modify it).
Looking at a comparison of all algorithms you can clearly see that AES is way faster than anything else. The reason for this is that I use AesCryptoServiceProvider
rather than AesManaged
. AesCryptoServiceProvider
uses the Windows OS API which has support for AES-NI whenever it's available, i.e. hardware accelerated encryption supported by the newer intel CPU:s (like the Core and Xeon series). AesManaged
is a completely managed implementation of the AES algorithm and will never make use of hardware acceleration.
AES
Looking closer at AES it's interesting to see that the CFB cipher mode does not perform very well with Hardware accelerated AES. I can only assume this mode is not supported with acceleration.
In this lib, I just ignored the Managed one since they provide the same result but slower. For the other algorithms, there is also CryptoServiceProviders
except Rijndael which only exists as Managed. I assume if you have older hardware, you might get accelerated RC2 and maybe DES & T-DES. I have not been able to confirm this yet.
You can also see that the ciphermode CFB seems to slow down the algorithms quite a lot except for smaller blocksize Rijndael. I think CFB should be avoided unless possibly you think a slow algorithm adds to your security.
Because of the good performance and becaue AES is the "new" FIPS standard from November 26, 2001 you should probably always go for AES_256_CBC
. This will ensure good security and performance. Some argue that Rijndael is the best one but in fact AES is a subset of Rijndael and it just supports higher blocksizes, if this means better encryption I can't say, but I can't justify the use of it because of the lack of hardware acceleration support. The only reason to use anything else than AES is for compability reasons.
Here follows the reult of performance testing for all symmetric algorithms.
Rijndael
DES
Triple DES
RC2
Points of Interest
What is salt and why/how is it normally used?
I have implemented salt for encrypted data in this crypto lib which I call AutoSalt, salt is however more commonly used when storing passwords for user accounts.
If you ever built an application which handles useraccounts and storing credentials, you probably came across salt if you implemented it properly. Hopefully you then realized, storing passwords for useraccounts in clear text or in a reversable encrypted manner is not a good idea. The way to go is to make a hashvalue of the passwords that cannot be reversed. This is however not really so secure either unless you use the proper hash algorithm to do it. Also, to further strengthen the hash, you should also use a salt value.
A salt is nothing more than a random set of data that is hashed in to the password. The salt value should be unique for every set of password that you hash and does not necessarily need to be secret, it can be stored together with the hashed value in a database table for example.
The benefit of using a salt like this is that the salted hash value that you store will be unique for every value you hash. So even if I store the same password for two different users, the hash will be different because of the salt. This will prevent an attacker that has stolen your database from using precompiling lookups, reverse lookups or rainbow tables that he could have prepared because he wouldn't know the hash beforehand. The attacker needs to do his attack on every single record which will be much harder and much more time consuming.
A common example of password hashing the wrong way would be to use an MD5 hash on a password without a salt. there are sites like http://www.md5online.org/ that can act as a reverse lookup where you put the hashed value and you get what the original data was. With a salt, this would not even have been so easy to reverse the password even though MD5 is not considered safe enough.
There is a lot more to say about password hashing which I feel is a bit off topic from my article. If you want to learn more about the proper way of salting, try reading more here.
I implement salts using Rfc2898DeriveBytes
. This is the recommended class to use and enables me to easily combine a password and salt value and give me bytes for both key
and init
vector. The salt value itself is generated with RNGCryptoServiceProvider
which is the recommended class to use for generating cryptographically safe data.
With the following code, you can generate a salt and a hash value that is perfectly safe to store for user account purposes.
byte[] salt = new byte[128];
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
rng.GetBytes(salt);
Rfc2898DeriveBytes key = new Rfc2898DeriveBytes("user password", salt, 100);
string passwordhash = Convert.ToBase64String(key.GetBytes(100));
What is a symmetric algorithm and which one do we use?
A symmetric algorithm (a.k.a. secret key algorithm) is an algorithm which uses the same key to encrypt or decrypt the data. The ones supported in .NET framework are the following:
- DES
- RC2
- TripleDES
- AES
- Rijndael
In my implementation, you can choose either of them. The standard today is AES and that is the reason I default to it in my library.
AES is a subset of Rijndael which is said to be the more secure. I suppose the reason is that Rijndael supports larger blocksizes and CFB cipher mode. I am not sure how much this improves security though. Rinjdael does not have any support for hardware acceleration in .NET which AES has, so I would say that you should probably always go for AES.
Cipher Mode
.NET supports many different cipher modes. Any of them will do as long as you avoid ECB. I think the following wikipage explains this pretty well.
Padding and padding mode
One common question/confusion is why the encrypted data is always longer than the original data. The reason is the BlockSize
parameter you can set on the SymmetricAlgorithm
. Depending on the Algorithm you choose, you get different blocksizes.
For example, if we have a Blocksize of 128bits (16 bytes), it means size of an encrypted data will always be equally dividable by 16 bytes, also there will always be a padding, so if the size of what you encrypt is already dividable by 16 you will get an extra block of padding. The padding will never be longer than the blocksize.
Example of 128 bit blocksize:
- If you encrypt 27 bytes, you will end up with 32 bytes of encrypted data.
- If you encrypt 32 bytes, you will end up with 48 bytes of encrypted data.
Example of 64 bit blocksize:
- If you encrypt 27 bytes, you will end up with 32 bytes of encrypted data.
- If you encrypt 32 bytes, you will end up with 40 bytes of encrypted data.
When you decrypt the data, this padding bytes will of course disappear.
In my implementation you can choose the padding you want, but unless you have a reason to change the padding I would stick to the default padding which is PKCS7
.
So far I havent found a reason to change it myself except when you set the padding mode to None
. In this case you need to handle the padding yourself in the data you feed to the cipher. It only accepts data which is a multiple of the blocksize. This is only useful for really advanced users.
If you are interested in digging into the padding more in detail, you can read more about it at http://en.wikipedia.org/wiki/Padding_(cryptography) and you can read about block size at http://en.wikipedia.org/wiki/Block_size_(cryptography).