Introduction
Microsoft's .NET framework has robust support for encryption in the System.Security.Cryptography namespace. Everything you need to perform encryption is available in that class, but it's difficult to understand unless you have a firm grasp of cryptographic theory. Over the last four months, I've struggled with the concepts and theory behind encrypting and decrypting data. I've wrapped all my derived knowledge into a class I call Encryption
. This class is heavily documented, string oriented, and most of all, simple! It's ideal for learning more about encryption.
Background
There are three essential cryptographic concepts represented in the Encryption
namespace. It's important that every developer understands these concepts before proceeding any further:
- Hashing
Hashes aren't encryption, per se, but they are fundamental to all other encryption operations. A hash is a data fingerprint - a tiny set of bytes that represents the uniqueness of a much larger block of bytes. Like fingerprints, no two should ever be alike, and a matching fingerprint is conclusive proof of identity. A full discussion of hashes is outside the scope of this article, but I highly recommend Steve Friedl's Illustrated Guide to Cryptographic Hashes for more background.
- Symmetric Encryption
In symmetric encryption, a single key is used for encrypting and decrypting the data. This type of encryption is quite fast, but has a severe problem: in order to share a secret with someone, they have to know your key. This implies a very high level of trust between people sharing secrets; if an unscrupulous person has your key-- or if your key is intercepted by a spy-- they can decrypt all the messages you send using that key!
- Asymmetric Encryption
Asymmetric encryption solves the trust problem inherent in symmetric encryption by using two different keys: a public key for encrypting messages, and a private key for decrypting messages. This makes it possible to communicate in secrecy with people you don't fully trust. If an unscrupulous person has your public key, who cares? The public key is only good for encryption; it's useless for decryption. They can't decrypt any of your messages! However, asymmetric encryption is very slow. It's not recommended for use on more than roughly 1 kilobyte of data.
These three concepts are heavily intertwined and always seen together in modern cryptography. They have different strengths and weaknesses; combining them offers a much higher level of security than can be achieved using a single method alone. For example, when digitally transmitting a check to your bank, all three of these methods are used:
Image reprinted from Entrust's Introduction to Cryptography and Digital Signatures PDF.
- A hash of the check is calculated.
- The hash is encrypted with our public key using asymmetric encryption.
- The encrypted hash is appended to the document.
- The document is encrypted using a unique one-time symmetric encryption key.
- The one-time symmetric encryption key is encrypted with the recipient's public key using asymmetric encryption.
- The encrypted key and encrypted document are transmitted to the recipient.
In order to open the check, these steps are simply performed in the reverse order by the recipient. Note that if any of these steps were missing, the transaction would have significant weaknesses that could be exploited!
Encryption.Hash
Let's start with the simplest operation-- Hashing the string "Hash Browns":
Dim h As New Encryption.Hash(Encryption.Hash.Provider.CRC32)
Dim d As New Encryption.Data("Hash Browns")
h.Calculate(d)
Console.WriteLine(".ToHex = '" & h.Value.ToHex & "'")
Console.WriteLine(".ToBase64 = '" & h.Value.ToBase64 & "'")
Console.WriteLine(".ToString = '" & h.Value.ToString & "'")
The unique data fingerprint of the string "Hash Browns" using the CRC32 algorithm is 32 bits or 4 bytes in length. We have a custom data type, Encryption.Data
, to aid us in converting those 4 bytes to and from familiar string representations:
.ToHex =
.ToBase64 =
.ToString =
It doesn't make much sense to display an array of raw bytes using the .ToString
method; that's shown only for illustrative purposes. You'll want raw byte values displayed either as Hexadecimal or Base64 encoded. If necessary, you can get to the raw byte representation via the Encryption.Data.Bytes
array.
The CRC32 hash is not a good choice for security work; it's optimized for speed and detection of machine transmission errors. It would be relatively easy for a knowledgeable human hacker to generate a string that produces the same CRC32 hash. Let's take a look at a slower, but more secure hash: SHA1.
Dim h As New Encryption.Hash(Encryption.Hash.Provider.SHA1)
Dim d As New Encryption.Data("Hash Browns")
Dim salt As New Encryption.Data("NaCl")
h.Calculate(d, salt)
Console.WriteLine(h.Value.ToHex)
Console.WriteLine(h.Value.ToBase64)
SHA1 produces a much longer and more tamper-resistant 160-bit hash code.
.ToHex =
.ToBase64 =
Notice the salt I added? Hashes are commonly used to avoid plain-text storage of passwords in a database. You calculate the hash of the password and store the hash instead of the actual password. When the user types in their password, hash it, then compare it against the stored hash in the database. It's clever, but there is a vulnerability: you can still mount a dictionary attack by hashing the English dictionary and matching it against the hashes stored in the database. We can prevent this by adding a salt-- a unique string-- to every password before hashing it. You'd typically salt with some arbitrary value from the same record, such as the record ID, user's birthday, or a GUID. It doesn't really matter what your salt is, as long as it makes the values unique. By adding the salt as shown above, we are effectively hashing the string "NaClHash Browns" instead of "Hash Browns". Good luck finding "NaClHash" in a dictionary!
Also note that string representations aren't particularly efficient; it takes 40 characters to represent the 160 bit (20 byte) hash in string using Hexadecimal, and 28 characters to represent that same hash using Base64 encoding. If you don't need to display your data in semi-human readable format, stick to binary formats. But the textual representations sure are convenient for use in XML or .config files!
We're not limited to Encryption.Data
byte arrays of fixed length. We can also calculate the hash of an IO.Stream
of any arbitrary size:
Dim sr As New IO.StreamReader("c:\test.txt")
Dim h As New Encryption.Hash(Encryption.Hash.Provider.MD5)
Console.WriteLine(".ToHex = '" & h.Calculate(sr.BaseStream).ToHex & "'")
sr.Close()
So the file test.txt has an MD5 hash of:
.ToHex =
Let's see what happens if we add a single space character to test.txt, and hash it again:
.ToHex =
One of the defining properties of a hash is that small changes in the source bytes produce big differences in the resulting hash bytes.
All hashes have the same purpose: to digitally fingerprint code. However, there are different speed and security tradeoffs for each Hash.Provider
:
Provider | Length (bits) | Security | Speed |
Hash.Provider.CRC32 | 32 | low | fast |
Hash.Provider.SHA1 | 160 | moderate | medium |
Hash.Provider.SHA256 | 256 | high | slow |
Hash.Provider.SHA384 | 384 | high | slow |
Hash.Provider.SHA512 | 512 | extreme | slow |
Hash.Provider.MD5 | 128 | moderate | medium |
Encryption.Symmetric
Symmetric encryption is the most familiar kind of encryption; you have a single secret key which is used to both encrypt and decrypt:
Dim sym As New Encryption.Symmetric(Encryption.Symmetric.Provider.Rijndael)
Dim key As New Encryption.Data("My Password")
Dim encryptedData As Encryption.Data
encryptedData = sym.Encrypt(New Encryption.Data("Secret Sauce"), key)
Dim base64EncryptedString as String = encryptedData.ToBase64
We now have some Rijndael encrypted bytes, expressed as a Base64 string. Let's decrypt them:
Dim sym As New Encryption.Symmetric(Encryption.Symmetric.Provider.Rijndael)
Dim key As New Encryption.Data("My Password")
Dim encryptedData As New Encryption.Data
encryptedData.Base64 = base64EncryptedString
Dim decryptedData As Encryption.Data
decryptedData = sym.Decrypt(encryptedData, key)
Console.WriteLine(decryptedData.ToString)
Like the Encryption.Hash
class, this also works for any arbitrarily-sized IO.Stream
as well as the fixed size Encryption.Data
:
Dim sym As New Encryption.Symmetric(Encryption.Symmetric.Provider.TripleDES)
Dim key As New Encryption.Data("My Password")
Dim fs As New IO.FileStream("c:\test.txt", IO.FileMode.Open,
IO.FileAccess.Read)
Dim br As New IO.BinaryReader(fs)
Dim encryptedData As Encryption.Data
encryptedData = sym.Encrypt(br.BaseStream, key)
br.Close()
Dim sym2 As New Encryption.Symmetric(Encryption.Symmetric.Provider.TripleDES)
Dim decryptedData As Encryption.Data
decryptedData = sym2.Decrypt(encryptedData, key)
There are a few things to remember when using the Encryption.Symmetric
class:
- All symmetric encryption is currently performed in memory. Be careful when encrypting extremely large files!
- .NET always chooses the largest available key size by default. If you want to manually specify a smaller key size, use the
.KeySizeBytes
or .KeySizeBits
properties. - The key is optional in the
.Encrypt
method. If you don't provide a key, a key of appropriate length will be auto generated for you and it can be retrieved via the .Key
property. It won't be fun to pronounce, because it'll be a randomly generated array of bytes, but it'll sure be hard to guess! - The
.InitializationVector
property is completely optional. The symmetric algorithms are block-oriented and seed the next block with the results from the previous block. This means the very first block has no seed, so that's where the IV comes in. It's annoying to have to remember both a password and an initialization vector to decrypt your data, and I don't think this is a serious weakness, so I recommend accepting the default initialization vector.
.NET provides four different Symmetric.Provider
algorithms; I would avoid the ones with shorter keys and known weaknesses:
Provider | Length (bits) | Known Vulnerabilities |
Symmetric.Provider.DES | 64 | yes |
Symmetric.Provider.RC2 | 40-128 | yes |
Symmetric.Provider.Rijndael | 128, 192, 256 | no |
Symmetric.Provider.TripleDES | 128, 192 | no |
Encryption.Asymmetric
Asymmetric encryption requires the use of two keys: one public, one private, together known as a "keyset". Let's generate a new keyset and encrypt some data:
Dim asym As New Encryption.Asymmetric
Dim pubkey As New Encryption.Asymmetric.PublicKey
Dim privkey As New Encryption.Asymmetric.PrivateKey
asym.GenerateNewKeyset(pubkey, privkey)
Dim secret As String = "ancient chinese"
Dim encryptedData As Encryption.Data
encryptedData = asym.Encrypt(New Encryption.Data(secret), pubkey)
Dim decryptedData As Encryption.Data
Dim asym2 As New Encryption.Asymmetric
decryptedData = asym2.Decrypt(encryptedData, privkey)
Note that we used the public key to encrypt, and the private key to decrypt.
Although you can certainly generate as many new public/private keysets as you want, you'll typically load an existing keyset. To facilitate loading and saving of keys, the Encryption.Asymmetric.PublicKey
and Encryption.Asymmetric.PrivateKey
classes support XML serialization via the .ToXml
and .FromXml
methods. They also support exporting to config file format via the .ToConfigSection
method, which returns a string suitable for cutting and pasting into the <appSettings>
section of your *.config file:
<appSettings>
<add key="PublicKey.Modulus"
value="3uWxbWSnlL2ntr/gcJ0NQeiWRfzj/72zIDuBW/TmegeodMdPUvI5vXur0fKp
6RbSU112oPf9o7hoAF8bdR9YOiJg6axZYKh+BxEH6pUPLbrtn1dPCUgTxlMeo0IhKvi
h1Q90Bz+ZxCp/V8Hcf86p+4LPeb1o9EOa01zd0yUwvkE=" />
<add key="PublicKey.Exponent"
value="AQAB" />
<add key="PrivateKey.P"
value="76iHZusdN1TYrTqf1gExNMMWbiHS7zSB/bi/xeUR0F3fjvnvsayn6s5ShM0jx
YHVVkRyVoH16PwLW6Tt2gpdYw==" />
<add key="PrivateKey.Q"
value="7hiVRmx0z1KERw+Zy86MmlvuODUsn2kuM06kLsSHbznSkYl5lekH9RFxFemNk
GGMBg8OT5+EVtWAOdto8KTJCw==" />
<add key="PrivateKey.DP"
value="ksvo/EqBn9XRzvH826npSQdCYv1G5gyEnzQeC4qPidEmUb6Yan12cWYlt4CsK
5umYGwWmRSL20Ufc+gnZQo6Pw==" />
<add key="PrivateKey.DQ"
value="QliLUCJsslDWF08blhUqTOENEpCOrKUMgLOLQJT3AGFmcbOTM9jJpNqFXovEL
NVhxVZwsHNM1z2LC5Q+O8BPXQ==" />
<add key="PrivateKey.InverseQ"
value="pjEtLwYB4yeDpdORNFxhFVXWZCqoky86bmAnrrG4+FvwkH/2dNe65Wmp62JvZ
7dwgPBIA+uA/LF+C1LXcXe9Aw==" />
<add key="PrivateKey.D"
value="EmuZBhlTYA9sVMX2nlfcSJ4YDSChFvluXDOOtTK/+UW4vi3aeFhcPTSDNo5/T
Cv+pbULoLHd3DHZJm61rjAw8jV5n09Trufg/Z3ybzUrAOzT3iTR2rvg7mNS2IBmaTyJg
emNKQDeFW81UOELVszUXNjhVex+k67Ma4omR6iTHSE=" />
</appSettings>
The private key is a superset of the public key; it can be used for both encryption and decryption, whereas the public key can only be used for encryption. Once a key is placed in the <appSettings>
section of your .config file, it will be used automatically; you no longer have to specify a private key in the .Decrypt
method:
Dim encryptedData As Encryption.Data
Dim decryptedData As Encryption.Data
Dim asym As New Encryption.Asymmetric
Dim asym2 As New Encryption.Asymmetric
Dim secret As String = "Michael Bolton"
encryptedData = asym.Encrypt(New Encryption.Data(secret))
decryptedData = asym2.Decrypt(encryptedData)
Console.WriteLine(decryptedData.ToString)
Note that we didn't specify any keys here; everything was automatically absorbed from the <appSettings>
section of the config file.
There are a few caveats when using Encryption.Asymmetric
:
- Microsoft's implementation of asymmetric encryption offers no choice of providers: you'll get RSA and you'll like it! You do get a choice of key sizes, though-- anywhere from 384 bits to 16,384 bits in steps of 8 bits. If you don't specify a size in the constructor, you'll get 1,024 bits by default. That should be more than enough for most uses.
- Asymmetric encryption is designed for small inputs. This is partly because asymmetric encryption is brutally slow, but it's also by design: depending on the key size you choose, you'll get an exception if you try to encrypt something too big! There are workarounds, but I don't recommend them. Follow best practices as defined at the top of this article; use asymmetric encryption to protect short stuff, like symmetric passwords or hashes.
The Annoying File Dependency in Encryption.Asymmetric
Unfortunately, Microsoft chose to provide some System.Security.Cryptography functionality through the existing COM-based CryptoAPI. Typically this is no big deal; lots of things in .NET are delivered via COM interfaces. However, there is one destructive side effect in this case: asymmetric encryption, which in my opinion should be an entirely in-memory operation, has a filesystem "key container" dependency:
Even worse, this weird little "key container" file usually goes to the current user's folder! I have specified a machine folder as documented in this Microsoft knowledge base article. Every time we perform an asymmetric encryption operation, a file is created and then destroyed in the C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys folder. It is simply unavoidable, which you can see for yourself by opening this folder and watching what happens to it when you make asymmetric encryption calls. Make sure whatever account .NET is running as (ASP.NET, etc.) has permission to this folder!
Conclusion
Encryption is a deep, complicated subject. I hope this article and the accompanying classes made it at least a little more approachable.
Please don't hesitate to provide feedback, good or bad! If you enjoyed this article, you may also like my other articles as well.
History
- Tuesday, April 19th, 2005
- Sunday, May 1st, 2005
- Minor bugfixes to article code.
- Corrected issue with byte array nulls and
Encoding.GetString
in C#.
- Monday, January 29th, 2007
- Straight port to .NET 2.0 and Visual Studio 2005.