Introduction
This blog posting reviews the types of encryption supported in .NET. This includes:
- File Encryption
- Windows Data Protection
- Hashing
- Symmetric Encryption
- Asymmetric Encryption
This review is spawned because it is an area of development I have worked in sparingly. This is the knowledge that I need. I started developing in C# during Alpha 1 of .NET in 2000. Fourteen years later I am finally getting around to taking the "
Exam 70-483; Programming in C#" certification example. You'd think I know it all by now. In preparation I skimmed an excellent course (I only skimmed because I already knew so much of the material) provided by
Microsoft Virtual Academy. The course was "
Programming in C# Jump Start".
It should be noted that .NET does not implement encryption and decryption. .NET takes advantage of Windows native encryption. This means that the encryption exposed by .NET is extremely fast because it is operating system supported.
Strings, UTF-16 and UTF-8
.NET strings are UNICODE and contain two bytes per-character -- UTF-16. Encryption is performed at the byte level. When text (the .NET String class) is encrypted, the String is converted to UTF-8 which means each character is represented by between one to four eight bit bytes. The System.Text namespace provides classes that can convert between encodings. When data is hashed or encrypted the following will be used to convert from String to byte array using:
const string data =
"If we are mark’d to die, we are enow. " +
"To do our country loss; and if to live, " +
"The fewer men, the greater share of honour.";
byte[] dataAsBytes = System.Text.Encoding.UTF8.GetBytes(data);
In the previous code snipped the Encoding class is used to get the UTF-8 byte representation of the string to be processed.
FYI: If you do not understand why the terminology "eight bit bytes" is used look up UTF-7, an encoding where bytes have seven bits.
File Encryption
Developers recognize that the key to working with files, folders and paths is the System.IO namespace and the File, Directory and Path static classes. These classes date back to .NET 1.0. As of .NET 2.0 two methods were added to the File class related to the encryption and decryption of files:
- Encrypt: Encrypts a file so that only the account used to encrypt the file can decrypt it.
public static void Encrypt(string path)
- Decrypt: Decrypts a file that was encrypted by the current account using the Encrypt method.
public static void Decrypt(string path)
It should be clear from the above documentation the ability to decrypt a file is tied to the user credentials used to encrypt the file.
An example program that both encrypts and decrypts a text file containing part of Shakespeare's "Henry V" Saint Crispin's day speech is as follows:
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace FileEncryption
{
class Program
{
static void Main(string[] args)
{
const string fileBody =
"If we are mark’d to die, we are enow. " +
"To do our country loss; and if to live, " +
"The fewer men, the greater share of honour.";
const string filename = "FileToDecrypt.txt";
var fullyQualifiedFilename =
Path.Combine(Path.GetTempPath(), filename);
// Create the file and encrypt the file
File.WriteAllText(fullyQualifiedFilename, fileBody);
File.Encrypt(fullyQualifiedFilename);
// Decrypt the file
File.Decrypt(fullyQualifiedFilename);
var currentBody = File.ReadAllText(fullyQualifiedFilename);
Trace.Assert(fileBody == currentBody);
}
}
}
Encrypting a file with File Explorer
Remember the encryption exposed by .NET is simply the encryption implemented by the operator system. To encrypt a file within File Explorer right block on the file and select properties and then select the General tab:
From the General tab click on the Advanced button:
Click on the "Encrypt contents to secure data" check box combined with clicking on OK, encrypts the file. When a file is encrypted the name of the file within File Explorer lightens in color. After Bing-ing this, I learned the color was "green" (for the majority who see colors -- encrypted files are green).
Windows Data Protection
Data of type byte array (byte[]) is what Windows Data Protection encrypts and decrypts. Like file encryption, Windows Data Protection ties the encryption/decryption process to a specific set of user credentials. As a technology Windows Data Protection was made available as of Windows 2000 and later (great since that was well over a decade ago). At the operating system level, Windows exposes the Data Protection API (DPAPI).
The .NET class used to access DPAPI is ProtectedData found in the System.Security.Cryptography namespace. The assembly for ProtectData is System.Security.dll. The ProtectedData class is a static class that exposes the following methods:
- Protect: encrypts the data in a specified byte array and returns a byte array that contains the encrypted data.
- Unprotect: decrypts the data in a specified byte array and returns a byte array that contains the decrypted data.
The signature for Project is as follows:
public static byte[] Protect(
byte[] userData,
byte[] optionalEntropy,
DataProtectionScope scope)
The first parameter, userData, is the byte array of data to be encrypted. The second parameter, optionalEntropy, is optional hence it can be specified as null. The optionalEntropy parameter increases the complexity of the encryption used. The more byte specified via the parameter, the better the encryption.
The values for DataProtectionScope are
- CurrentUser: only the current user can use Unprotect or the underlying DPAPI to decrypt the data.
- LocalMachine: any process on the machine can use Unprotect or the underlying DPAPI to decrypt the data. This ability for any process to decrypt is because the file is encrypted using the machine's context.
The signature for Unproject is as follows:
public static byte[] Unprotect(
byte[] encryptedData,
byte[] optionalEntropy,
DataProtectionScope scope)
The first parameter, encryptedData, is the byte array previously encrypted by Protect. The values for the optionalEntropy and scope must match the values specified when Protect was invoked to originally encrypt the data.
When developing an application that makes use of ProtectedData the System.Security.dll assembly must be reference by the project.
An example program that both protects (encrypts) and un-protects (decrypts) a byte array containing part of Shakespeare's "Henry V" Saint Crispin's day speech is as follows:
using System;
using System.IO;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Security.Cryptography;
namespace WindowsDataProtection
{
class Program
{
static void Main(string[] args)
{
const string data =
"If we are mark’d to die, we are enow. " +
"To do our country loss; and if to live, " +
"The fewer men, the greater share of honour.";
byte[] dataAsBytes =
System.Text.Encoding.UTF8.GetBytes(data);
byte[] makeItMoreComplex = { 1, 3, 2, 4, 5, 7, 6, 8 };
var backToString =
System.Text.Encoding.Unicode.GetString(dataAsBytes);
var thisIsEncyrpted = ProtectedData.Protect(
dataAsBytes,
makeItMoreComplex,
DataProtectionScope.CurrentUser);
// The string created here is garbage -- encrypted bytes
// converted to text
var backToStringMangled =
System.Text.Encoding.Unicode.GetString(thisIsEncyrpted);
var thisIsDecyrpted = ProtectedData.Unprotect(
thisIsEncyrpted,
makeItMoreComplex,
DataProtectionScope.CurrentUser);
var backToStringCorrect =
System.Text.Encoding.UTF8.GetString(thisIsDecyrpted);
Trace.Assert(data == backToStringCorrect);
}
}
}
Hashing
A hash is signature computed on a sequence of data. The signature is designed to detect if the data has been modified which often means to detect if the data has been corrupted. Two pieces of data may generate an identical hash (which should be unlikely given a complex enough hashing algorithm). It is unlikely that a piece of data that has been modified will generate identical hashes both before and after modification.
The way a great many of us learned about real-world hashing was with downloading an ISO (DVD image) or EXE from MSDN. The EXE and ISO files were often hundreds of megabytes and a corruption means the ISO could not be mounted or the EXE could not be run. To detect if there was a corruption each ISO/EXE is hashed using SHA1. The following screenshot is from the MSDN download page where the download is 2865 mega bytes in size and the SHA1 hash computed on the ISO downloaded is provided directly on the page:
The value for the SHA1 hash is 41843D4B92CF85008715860031AB6F2FACEC5373 for the reference ISO.
The support for hashing in .NET is provided by the System.Security.Cryptography namespace. There are different possible hashing algorithms (MD5, SHA1, etc.). Each of these algorithms is implemented using the HashAlgorithm as a base class. The standard list of providers derived from the HashAlgorithm base class is as follows:
System.Object
System.Security.Cryptography.HashAlgorithm
System.Security.Cryptography.KeyedHashAlgorithm
System.Security.Cryptography.MD5
System.Security.Cryptography.RIPEMD160
System.Security.Cryptography.SHA1
System.Security.Cryptography.SHA256
System.Security.Cryptography.SHA384
System.Security.Cryptography.SHA512
The following code demonstrates how to compute a hash using SHA512 and how to compare hashes:
using System;
using System.IO;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Security.Cryptography;
namespace Hashing
{
class Program
{
static void Main(string[] args)
{
const string data =
"If we are mark’d to die, we are enow. " +
"To do our country loss; and if to live, " +
"The fewer men, the greater share of honour.";
byte[] dataAsBytes =
System.Text.Encoding.UTF8.GetBytes(data);
var hasher = SHA512.Create();
byte [] hashOriginal = hasher.ComputeHash(dataAsBytes);
// String is invariant to use a StringBuilder so
// that the text can be modified
var dataToMangle = new System.Text.StringBuilder(data);
dataToMangle.Replace("country", "city");
var mangledData = dataToMangle.ToString();
byte[] mangledDataAsBytes =
System.Text.Encoding.UTF8.GetBytes(mangledData);
byte[] hashMangled =
hasher.ComputeHash(mangledDataAsBytes);
Trace.Assert(Enumerable.SequenceEqual(
hashOriginal, hashMangled) == false);
}
}
}
The comparison between the hash byte arrays is performed by Enumerable.SequenceEqual (available .NET 3.5 and later). This static method is documented within MSDN to: Determine whether two sequences are equal by comparing the elements by using the default equality comparer for their type.
The full documentation for SequenceEqual can be found in MSDN as follows:
Symmetric Encryption
With regard to a definition of symmetric and asymmetric encryption, Microsoft has a knowledge base article:
Description of Symmetric and Asymmetric Encryption. Symmetric encryption is where the encrypting application and the decrypting have the same key. If a client encrypts with the password, MorrisseyMarr then passes the encrypted data to a server, the server must decrypt using the same password, MorrisseyMarr.
For .NET developers the System.Security.Cryptography namespace provides support for symmetric encryption. Each symmetric encryption provider is derived from the SymmetricAlgorithm base class:
System.Object
System.Security.Cryptography.SymmetricAlgorithm
System.Security.Cryptography.Aes
System.Security.Cryptography.DES
System.Security.Cryptography.RC2
System.Security.Cryptography.Rijndael
System.Security.Cryptography.TripleDES
Each of the previous classes is found in mscorlib.dll. There is no need to create a reference within a project to access this assembly. An example of TripleDes using a 16 byte key (128-bit key) to encrypt and decrypt symmetrically is as follows:
using System;
using System.IO;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Security.Cryptography;
namespace SymmetricEncryption
{
class Program
{
static void Main(string[] args)
{
// 16 bytes, a.k.a. 128 bits
const string topSecretSecurityKey = "0123456789012345";
byte[] topSecretSecurityKeyReady =
System.Text.UTF8Encoding.UTF8.GetBytes(
topSecretSecurityKey);
const string data =
"If we are mark’d to die, we are enow. " +
"To do our country loss; and if to live, " +
"The fewer men, the greater share of honour.";
byte[] dataAsBytes =
System.Text.Encoding.UTF8.GetBytes(data);
var provider = new TripleDESCryptoServiceProvider();
provider.Key = topSecretSecurityKeyReady;
provider.Mode = CipherMode.CBC;
provider.Padding = PaddingMode.PKCS7;
byte[] encryptedData = null;
using (var encryptor = provider.CreateEncryptor())
{
encryptedData = encryptor.TransformFinalBlock(
dataAsBytes,
0,
dataAsBytes.Length);
}
Trace.Assert(Enumerable.SequenceEqual(
dataAsBytes,
encryptedData) == false);
byte[] decryptedData = null;
using (var decryptor = provider.CreateDecryptor())
{
decryptedData = decryptor.TransformFinalBlock(
encryptedData,
0,
encryptedData.Length);
}
Trace.Assert(Enumerable.SequenceEqual(
dataAsBytes,
decryptedData) == true);
}
}
}
Asymmetric Encryption
The standard mechanism used for encryption is what is called asymmetric. In this model a public key is made available publicly. Any application needing to send encrypted data can use the public key to encrypt data. The rub is, the public key is incapable of decrypting the data. The decrypting application contains a private key that is paired with the public key. Only the public key can decrypt data encrypted with the public key. Unlike symmetric encryption where both parties have the master key, with asymmetric encryption there is only one copy of the master key -- the private key.
The System.Security.Cryptography namespace contains the algorithms that implement asymmetric encryption. All providers implementing this encryption are derived from the AsymmetricAlgorithm base class:
System.Object
System.Security.Cryptography.AsymmetricAlgorithm
System.Security.Cryptography.DSA
System.Security.Cryptography.ECDiffieHellman
System.Security.Cryptography.ECDsa
System.Security.Cryptography.RSA
Each of the previous classes is found in mscorlib.dll or System.Core.dll. There is no need to create a reference within a project to access this assembly since these assemblies are normally referenced. An example of a class implemented using TripleDes and a 256 byte key (2048-bit key) to encrypt and decrypt symmetrically is presented below.
A helper class is needed into which to place the public/private keys which are generated from the same aysmetric provider instance:
using System;
namespace AsymmetricEncryption
{
public class PublicPrivateKeys
{
public PublicPrivateKeys(string publicKey, string privateKey)
{
Public = publicKey;
Private = privateKey;
}
public string Public { get; private set; }
public string Private { get; private set; }
}
}
The class gets the keys and exposes Encrypt/Decrypt methods is as follows:
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Security.Cryptography;
namespace AsymmetricEncryption
{
public class AsymmetricEncryption
{
private const int _keySizeBits = 2048;
private static bool _optimalAsymmetricEncryptionPadding = false;
public static PublicPrivateKeys GetKeys()
{
using (var provider = new RSACryptoServiceProvider(_keySizeBits))
{
// Generate public key and private key at the same time or the will not be valid
var publicKey = provider.ToXmlString(false);
var publicAndPrivateKey = provider.ToXmlString(true);
return new PublicPrivateKeys(publicKey, publicAndPrivateKey);
}
}
public static int GetMaxDataLength(int keySize)
{
if (_optimalAsymmetricEncryptionPadding)
{
return ((keySize - 384) / 8) + 7;
}
return ((keySize - 384) / 8) + 37;
}
public static bool IsKeySizeValid(int keySize)
{
return keySize >= 384 &&
keySize <= 16384 &&
keySize % 8 == 0;
}
public static string Encrypt(string text, string publicKeyXml)
{
var encrypted = Encrypt(Encoding.UTF8.GetBytes(text), publicKeyXml);
return Convert.ToBase64String(encrypted);
}
public static byte[] Encrypt(byte[] data, string publicKeyXml)
{
int maxLength = GetMaxDataLength(_keySizeBits);
if (data.Length > maxLength)
{
throw new ArgumentException(String.Format("Maximum data length is {0}", maxLength), "data");
}
if (!IsKeySizeValid(_keySizeBits))
{
throw new ArgumentException("Key size is not valid", "keySize");
}
if (String.IsNullOrEmpty(publicKeyXml))
{
throw new ArgumentException("Key is null or empty", "publicKeyXml");
}
using (var provider = new RSACryptoServiceProvider(_keySizeBits))
{
provider.FromXmlString(publicKeyXml);
return provider.Encrypt(data, _optimalAsymmetricEncryptionPadding);
}
}
public static string Decrypt(string text,
string publicAndPrivateKeyXml)
{
var decrypted = Decrypt(Convert.FromBase64String(text),
publicAndPrivateKeyXml);
return Encoding.UTF8.GetString(decrypted);
}
public static byte[] Decrypt(byte[] data,
string publicAndPrivateKeyXml)
{
if (!IsKeySizeValid(_keySizeBits))
{
throw new ArgumentException(
"Key size is not valid", "keySize");
}
if (String.IsNullOrEmpty(publicAndPrivateKeyXml))
{
throw new ArgumentException(
"Key is null or empty", "publicAndPrivateKeyXml");
}
using (var provider =
new RSACryptoServiceProvider(_keySizeBits))
{
provider.FromXmlString(publicAndPrivateKeyXml);
return provider.Decrypt(
data, _optimalAsymmetricEncryptionPadding);
}
}
}
}
The method that demonstrates keys being retrieved, data being encrypted and decrypted is as follows:
using System;
namespace AsymmetricEncryption
{
public class PublicPrivateKeys
{
public PublicPrivateKeys(string publicKey, string privateKey)
{
Public = publicKey;
Private = privateKey;
}
public string Public { get; private set; }
public string Private { get; private set; }
}
}
In the real world, the public key would be distributed to the entities that needed to encrypt data. The private key would be kept in a secure location and the encrypt/decrypt kept entirely separate from each other.