.NET Encryption Simplified
By Jeff Atwood | Published: 21 June 2005 |
Reader Level: Beginner
A simple, string-oriented class for symmetric encryption, asymmetric encryption,
and hashing.
Download source files - 26.2 Kb
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 = 'FDBFBC6D'
.ToBase64 = '/b+8bQ=='
.ToString = 'y¿¼m'
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 = '95CF26B3BB0F377347B6D414951456A16DD0CF5F'
.ToBase64 = 'lc8ms7sPN3NHttQUlRRWoW3Qz18='
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 = '92C7C0F251D98DEA2ACC49B21CF08070'
Let's see what happens if we add a single space character to test.txt,
and hash it again:
.ToHex = 'FADECF02C2ABDC7B65EBF2382E8AC756'
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.
About Jeff Atwood
My name is Jeff Atwood. I live in Durham, NC with my wife, two cats, and far
more computers than I care to mention. My first computer was the
Texas Instruments TI-99/4a. I've been a Microsoft Windows developer
since 1992; primarily in VB. I am particularly interested in best practices and
human factors in software development, as represented in my
recommended developer reading list. I also have a coding and human
factors related blog at www.codinghorror.com.