What You Will Learn in This Chapter
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
You can find the code downloads for this chapter at www.wrox.com/remtitle.cgi?isbn=1118612094 on the Download Code tab. The code is in the chapter12 download and individually named according to the names throughout the chapter.
When you deal with sensitive data, you must protect it from unauthorized access or modification, and for that you need to use techniques such as encryption, digital signatures, and hashing of data. One application of digital signatures and hashing of data used often by .NET is with strong name assemblies.
This chapter begins by going through different technologies that you can use to ensure privacy, integrity, and authenticity of your data. You will look at both symmetric and asymmetric algorithms. In the second part of this chapter you will look in details at how you can ensure the integrity of you assemblies by using digital signatures.
Table 12-1 introduces you to the exam objectives covered in this chapter.
Objective | Content Covered |
Perform symmetric and asymmetric encryption | Choose an appropriate encryption algorithm. This includes discussing the symmetric and asymmetric algorithms. Manage and create certificates. This includes working with different kind of certificates and certificates store. Implement key management. This includes discussing the options available in .NET to store the encryption keys. Implement the System.Security namespace. This includes discussing the classes and interfaces available in the System.Security namespace. Hash data. This includes discussing the options available to hash data and to create digital signatures. Encrypt streams. This includes using the CryptoStream class to encrypt streams |
Manage assemblies | Version assemblies. This includes choosing a versioning scheme for the assembly. Sign assemblies using strong names. This includes signing the assemblies from Visual Studio, as well as using the sn.exe. Implement side-by-side hosting. This includes discussing techniques you can use to have different version of the same assembly on the same machine at the same time. Put an assembly in the global assembly cache. This includes using different tools to deploy the assemblies in the global assembly cache. |
Encryption is the process of transforming plain data in a way that makes it harder for an unauthorized person to make sense of it. The encrypted data is called ciphertext. Decryption is the reverse process, meaning that having the ciphertext, you must apply a transformation to it to get back the original information. The harder it is to decrypt the ciphertext, the better the algorithm. You might have reacted to saying “harder” to make sense of it instead of saying “impossible.” The reason for that is because any encrypted data can be decrypted eventually, but some algorithms are so complicated that it can take a long time (such as hundreds of years) to decrypt the data, so the information is useless. Cryptography is the practice and study of encryption and decryption techniques.
Romans used simple algorithms such as transposition. They replaced every letter with another one, like the next one in the alphabet, or the one found three positions after the current one. But this was just a simple algorithm. To decrypt the message, you only had to figure out the algorithm used (transposition in this example) and the algorithm parameters (how many positions were used for the transposition). In time the algorithms evolved, making it harder and harder to decrypt the data. A notable effort before computers appeared was the German machine Enigma that used an electromechanical rotor cipher to encrypt and decrypt secret messages.
With the invention of computers, the encryption became more and more advanced, and with today’s technologies the algorithms are open because their parameters are the ones improving the security of any encrypted data. Today’s encryption techniques make heavy use of mathematics.
Sometimes you might not need to hide the data, but to make sure that the data is not tampered with, or that the data comes from the right source. If you want to ensure that the data is not tampered with, you can use a Secured Hash Algorithm (SHA), whereas for authenticity of the data you can use a Message Authentication Code (MAC) algorithm. So let’s assume that you want to send an order to a supplier. To make sure that the order doesn’t get altered on its way to the supplier, you can add the SHA signature to the message. If you want to guarantee the identity of the sender of the order as well as the integrity of the order, then you would use a MAC.
You saw that encryption and decryption protect data from unauthorized access. Encryption can be done in two ways. One way is called symmetric encryption, or shared secret, and the second one is called asymmetric encryption or public key. (Both types of encryption are described in more detail in the following sections.)
Microsoft implemented some of the existing algorithms in .NET, which are implemented in three ways:
All cryptography classes are defined in the System.Security.Cryptography namespace and are part of the core .NET library.
As mentioned symmetric encryption is also known as shared secret encryption, and that is because the encryption of the data is done with an encryption key, a byte array, and the same key is used to decrypt the data. The symmetric algorithms rely on the fact that only an authorized person has access to the encryption key. The main drawback of the symmetric encryption is that if the encryption key becomes compromised, the data will not be secured.
The symmetric algorithms that come with .NET use a chaining mode called cipher block chaining. This kind of algorithm works in the following way. If the data is bigger than a predefined size, called block size, the data is split in blocks of that predefined size. The first block is encrypted using a random block of data of the same size, called initialization vector (IV), and the encryption key. The next block is encrypted using the result of the previous encrypted block instead of the IV and the same encryption key, and so on until it reaches the last block. The block size is determined by the algorithm used. If the last block is less than that size, it will be padded with data so it will have the same size as the size used by the algorithm. To decrypt the cipher text, you must have the IV and the key. The IV doesn’t need to be kept secret, but the private key does. You will see later in this chapter how the encryption key can be handled.
.NET Framework implements five different symmetric algorithms, as shown in Table 12-2.
Algorithm Short Name | Description |
Aes | Advanced Encryption Standard (AES). There are two classes implementing this algorithm: AesManaged and AesCryptoServiceProvider. |
DES | Data Encryption Standard algorithm implemented by DESCryptoServiceProvider. |
RC2 | Rivest Cipher (or Ron’s code) algorithm implemented by RC2CryptoServiceProvider. |
Rijndael | Rijndael algorithm implemented by RijndaelManaged. |
TripleDES | Triple Data Encryption Standard (DES) algorithm implemented by TripleDESCryptoServiceProvider. |
All the classes mentioned in Table 12-2 inherit from the System.Security.Cryptography.SymmetricAlgorithm class. This class contains properties and methods that are useful when working with symmetric algorithms. Tables 12-3 and 12-4 list the properties and methods of that class.
Property | Description |
BlockSize | Gets or sets the size of the block used by the cryptographic operation. The block size is specified in bits and represents the basic unit of data that can be encrypted or decrypted in one operation. Messages longer than the block size are split into blocks of this size; messages shorter than the block size are padded with extra bits until they reach the size of a block. The algorithm used determines the validity of the block size. |
FeedbackSize | Gets or sets the size of the feedback size used by the cryptographic operation. The feedback size represents the amount of data in bits that is fed back to the next encryption or decryption operation. The feedback size must be lower than the block size. |
IV | Gets or sets the IV. Whenever you create a new instance of a symmetric algorithm, the IV is set to a new random value. You can generate one as well by calling the GenerateIV method. The size of the IV property must be the same as the BlockSize property divided by eight. |
Key | Gets or sets the secret key used by the algorithm. The secret key has to be the same for both encryption and for decryption. For a symmetric algorithm to be successful, the secret key must be kept secret. The valid key sizes are specified by the particular symmetric algorithm implementation and are listed in the LegalKeySizes property. |
KeySize | Gets or sets the size of the secret key used by the symmetric algorithm. The valid key sizes are specified in bits by the particular symmetric algorithm implementation and are listed in the LegalKeySizes property. |
LegelBlockSizes | Gets the block sizes in bits that are accepted by the algorithm. |
LegalKeySizes | Gets the key sizes in bits that are accepted by the algorithm. |
Mode | Gets or sets the mode for operation of the algorithm. See the System.Security.Cryptography.CipherMode enumeration for a description of specific modes. |
Padding | Gets or sets the padding mode used by the algorithm. See the System.Security.Cryptography.PaddingMode enumeration for a description of specific modes. |
Method | Description |
Clear | Releases all resources used by the SymmetricAlgorithm class. You need to call this method to clear all the resources allocated by the algorithm to ensure that no sensitive data remains in the memory when you finish with the cryptographic object. Do not rely on garbage collector to clear the data. |
Create() | This static method creates a new cryptographic using the default algorithm, which in .NET 4.5 is RijndaelManaged. |
Create(String) | This static method creates a new cryptographic object using the specified cryptographic algorithm. The name of the algorithm can be either one of the names on the left column of Table 12-2 or the name of the type itself, with or without the namespace. Aes corresponds to the AesCryptoServiceProvider algorithm. If you want to use the managed version of the algorithm, you need to specify AesManaged. |
CreateDecryptor() | Creates a decryptor object using the Key and IV currently set in the properties. |
CreateDecryptor(Byte[], Byte[]) | Creates a decryptor object using the Key and IV values specified as parameters. |
CreateEncryptor() | Creates an encryptor object using the Key and IV currently set in the properties. |
CreateEncryptor(Byte[], Byte[]) | Creates an encryptor object using the Key and IV properties specified as parameters. |
GenerateIV | Generates a random IV to be used by the algorithm. Normally there is no need to call this method. |
GenerateKey | Generates a random Key to be used by the algorithm. The secret key has to be the same for both encryption and for decryption. For a symmetric algorithm to be successful, the secret key must be kept secret. The valid key sizes are specified by the particular symmetric algorithm implementation and are listed in the LegalKeySizes property. |
ValidKeySize | Returns true if the specified key size is valid for this specific algorithm. |
The workflow of encrypting plain text into chipper text is straightforward:
The code should look like this:
byte[] EncryptData(byte[] plainData, byte[] IV, byte[] key) {
SymmetricAlgorithm cryptoAlgorythm = SymmetricAlgorithm.Create();
ICryptoTransform encryptor = cryptoAlgorythm.CreateEncryptor(key, IV);
byte[] cipherData = encryptor.TransformFinalBlock(plainData, 0,
plainData.Length);
return cipherData;
}
The workflow of decrypting chipper text to get back the plain text is straightforward as well:
The code should look like this:
byte[] DecryptData(byte[] cipherData, byte[] IV, byte[] key) {
SymmetricAlgorithm cryptoAlgorythm = SymmetricAlgorithm.Create();
ICryptoTransform decryptor = cryptoAlgorythm.CreateDecryptor(key, IV);
byte[] plainData = decryptor.TransformFinalBlock(cipherData, 0,
cipherData.Length);
return plainData;
}
The biggest challenge of symmetric encryption algorithms is to keep the key secret. If the data doesn’t have to leave the machine, you must save it somehow in a safe place so that only you can get access to it when you need to decrypt the data. You see later in this chapter in the “Implementing Key Management” section how to solve that.
If you need to send the data across the wire, make sure that the other party has the key as well. Transmitting the key needs to be done securely so that no unauthorized person gets access to it; otherwise, the whole data becomes compromised.
An alternative to symmetric encryption is to use asymmetric encryption. The main reason to use asymmetric encryption is to avoid sharing the encryption key, which is considered a vulnerability. Asymmetric encryption uses two mathematically related keys that complement each other, such as whatever is encrypted with one key can be decrypted only with the other key. One key is made public, and is known as the public key, by the receiving party, so whoever wants to transmit secured data can encrypt the data. Make sure at the same time only the receiving party can decrypt the data by using the other key, known as the private key. (The mathematics behind generating the two keys is outside the scope of this book.)
The main disadvantage of the asymmetric encryption is that it is slower than the symmetric encryption, but the biggest advantage is that there is no need to have a shared secret for the algorithm to work.
.NET Framework implements five different symmetric algorithms, as shown in Table 12-5.
Algorithm Short Name | Description |
DSA | Digital Signature algorithm. Used to create digital signatures that help protect the integrity of data. There is one class implementing this algorithm: DSACryptoServiceProvider. Use DSA only for compatibility with legacy applications and data. |
ECDiffieHellman | Elliptic Curve Diffie-Hellman algorithm implemented by ECDiffieHellmanCng. |
ECDsa | Elliptic Curve Digital Signature Algorithm (ECDSA) algorithm implemented by ECDsaCng. |
RSA | RSA algorithm implemented by RSACryptoServiceProvider. |
In .NET all classes that implement an asymmetric algorithm inherit from System.Security.Cryptography.AsymmetricAlgorithm.
The workflow of encrypting data using asymmetric encryption follows:
The workflow of decrypting data using asymmetric encryption follows:
If the data were changed or it not encrypted using the corresponding public key, a CryptographicException will be thrown.
There are two ways to handle the keys. The first is to send the key directly; the second is to save the keys in a cryptographic service provider container. How to handle the key will be discussed later in this chapter in the “Implementing Key Management” section. To see how those two are implemented, look at the code lab at the end of this section.
One common scenario is to use asymmetric encryption to exchange the keys needed for symmetric encryption using the following workflow:
Another common scenario is to use the asymmetric encryption to digitally sign data, ensuring in this way both the authenticity and the identity.
As discussed in Chapter 9, “Working with Data,” streams represent a file, an I/O device, or a communication channel and can perform three fundamental operations:
One important property of streams in .NET is that they can be chained by feeding the output data from a stream into the input of another stream. Sometimes you might want to encrypt the data the goes through those streams, in order to ensure the privacy or integrity of the data. You can either encrypt the data before it is sent through the stream, or you can just chain the streams so one of them will be in charge of encryption. Remember that one of the streams classes presented in Chapter 9 was CryptoStream, which you can use to encrypt or decrypt data coming through it.
The workflow of encrypting streams is straightforward:
The code should look like this:
byte[] EncryptString(string plainData, byte[] IV, byte[] key) {
SymmetricAlgorithm cryptoAlgorythm = SymmetricAlgorithm.Create();
ICryptoTransform encryptor = cryptoAlgorythm.CreateEncryptor(key, IV);
byte[] cipherData = new byte[0];
using (MemoryStream msEncrypt = new MemoryStream()) {
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt,
encryptor,
CryptoStreamMode.Write)) {
StreamWriter swEncrypt = new StreamWriter(csEncrypt);
swEncrypt.Write(plainData);
swEncrypt.Close();
csEncrypt.Clear();
cipherData = msEncrypt.ToArray();
}
}
return cipherData;
}
The workflow of decrypting streams is straightforward as well:
The code should look like this:
string DecryptString(byte[] cipherData, byte[] IV, byte[] key) {
SymmetricAlgorithm cryptoAlgorythm = SymmetricAlgorithm.Create();
ICryptoTransform decryptor = cryptoAlgorythm.CreateDecryptor(key, IV);
string plainText = string.Empty;
using (MemoryStream msDecrypt = new MemoryStream(cipherData)) {
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt,
decryptor,
CryptoStreamMode.Read)) {
StreamReader srDecrypt = new StreamReader(csDecrypt);
plainText = srDecrypt.ReadToEnd();
srDecrypt.Close();
csDecrypt.Clear();
}
}
return plainText;
}
Hashing is the process of mapping binary data of a variable length to a fixed size binary data. Applying the same hash function to two identical data structures yields the same result. Hashing functions are used in several scenarios:
The strength of the cryptographic hash is that it is improbable that two different inputs generate the same hash. Two passwords that are not the same and differ little from each other can produce two completely different hashes.
There are two kind of hashing algorithms: with or without a key. The algorithms without keys are used only to calculate secure hashes for data to ensure integrity, whereas the keyed algorithms are used together with a key as a MAC for both integrity and authenticity.
.NET Framework comes with implementations for different algorithms, as shown in Table 12-6.
Algorithm Short Name | Description |
SHA1 | Implementation of SHA algorithm with a resulting hash size of 160 bits |
SHA256 | Implementation of SHA algorithm with a resulting hash size of 256 bits |
SHA512 | Implementation of SHA algorithm with a resulting hash size of 512 bits |
SHA384 | Implementation of SHA algorithm with a resulting hash size of 384 bits |
MD5 | Implementation of MD5 hash algorithm |
RIPEMD160 | Implementation of RIPEMD hash algorithm |
In .NET all hashing algorithms inherit from the System.Security.Cryptography.HashAlgorithm abstract class. Tables 12-7 and 12-8 list the properties and methods of the HashAlgorithm class.
Property | Description |
CanReuseTransform | Read-only property that returns true if the current transform can be reused |
CanTransformMultipleBlocks | Read-only property that returns true if multiple blocks can be transformed |
Hash | Read-only property that returns the calculated hash code |
HashSize | Read-only property that returns, in bits, the size of the computed hash code |
InputBlockSize | Read-only property that returns the size of the input block |
OutputBlockSize | Gets the size of the output block |
Method | Description |
Clear | Releases all resources used by the HashAlgorithm class. |
ComputeHash(Byte[]) | Calculates the hash for the a byte array. |
ComputeHash(Stream) | Calculates the hash for the a Stream object. |
ComputeHash(Byte[], Int32, Int32) | Calculates the hash for a region of a byte array. |
Create() | This static method creates a new cryptographic using the default algorithm, which in .NET 4.5 is SHA1. |
Create(String) | This static method creates a new hash algorithm object using the specified algorithm. The name of the algorithm can be either one of the names on the left column of Table 12-6 or the name of the type itself, with or without the namespace. |
TransformBlock | Calculates the hash for the specified region of the inputBuffer byte array and copies the result to the specified region of the outputBuffer byte array. |
TransformFinalBlock | Calculates the hash for the specified region of the inputBuffer byte array. |
Normally, you use only Create and the ComputeHash methods. TransformBlock and TransformFinalBlock methods are used when you want to compute the hash for portions of your data.
The keyed hashing algorithms inherit from the System.Security.Cryptography.KeyedHashAlgorithm class, which has only one extra property compared with the HashAlgorithm class, as shown in Table 12-9.
Property | Description |
Key | Read/write property representing the key to be used by the hash algorithm. If you attempt to change the key after the hashing has begun, a CryptographicException is thrown. |
The workflow of hashing data is as follows:
string ComputeHash(string input)
{
HashAlgorithm sha = SHA256.Create();
byte[] hashData = sha.ComputeHash(Encoding.Default.GetBytes(input));
return Convert.ToBase64String(hashData);
}
The workflow of verifying a hash for data follows:
bool VerifyHash(string input, string hashValue) {
HashAlgorithm sha = SHA256.Create();
byte[] hashData = sha.ComputeHash(Encoding.Default.GetBytes(input));
return Convert.ToBase64String(hashData) == hashValue;
}
Although the communication might seem secure at times, when two parties communicate, they need to make sure that they are talking with the right partner. For instance, when you want to do a bank transaction via the Internet, you need to make sure that you are on your bank site and not on some website that is spoofing the identity of your bank. You also want to know that the communication is secured. For web applications there are two protocols that solve this problem: Transport Layer Security (TLS) and Secure Socket Layer (SSL).
public static void Run() {
string input = "Data to be hashed!";
string hash = ComputeHash(input);
bool sameHash = VerifyHash(input, hash);
Console.WriteLine("Input:{0}", input);
Console.WriteLine("Hash: {0}", hash);
Console.WriteLine("Same hash: {0}", sameHash);
}
Both encrypt data and ensure data authenticity. For authenticity they use Public Key Infrastructure (PKI) (the infrastructure needed to handle digital certificates). PKI uses a notion called Certificate Authority (CA). A CA is an entity that issues digital certificates. Digital certificates bind a public key with an identity. By doing this, when two parties wants to communicate and the sending party wants to make sure that it talks with the right party, the sending party can use PKI to verify the identity of the receiving party.
The principle is simple. You can either choose to trust the other party directly, but this might become cumbersome if you have to do that for every party, or you can choose to trust a third party instead that only verifies the identity of different entities. This second choice is a hierarchic one, meaning the number of entities you choose to trust is limited. Those entities can verify the identity of other entities, which become trusted entities and so on, until one of them will trust the entity you want to communicate with and confirm its identity.
The top-level certificates that you choose to trust are called root certificates. Normally, you won’t add any root certificates. They come with your Windows installation or via Windows Updates. If you want to see which roots certificates your computer is set up to trust by default, go to Control Panel and open the Internet Options dialog. From there open the Content tab, and press the Certificates button; then choose the Trusted Root Certification Authorities tab. A dialog similar with the one in Figure 12-2 appears. As you can see, the list is long, but it can save you a lot of trouble if you want to have secure communication.
If a site now wants to guarantee its identity, it must verify only its identity with one of the entities you chose to trust. One example is Microsoft. On your computer, as you can see in the previous figure, there is a Root Certificate Authority called GTE CyberTrust Global Root. If you point your browser to https://www.microsoft.com (note the https) and you want to see the certificate of the site and go to the Certification Path tab, a dialog similar with the one in Figure 12-3 appears.
As you can see, www.microsoft.com is trusted by Microsoft Secure Server Authority, which is trusted by Microsoft Internet Authority, which is trusted by GTE CyberTrust Global Root, which you can choose to trust, as one of your Root Certificate Authorities.
The standard used by the PKI is X.509, which specifies the format the PKIs, the Certificate Revocation List (CLR), attributes for the certificates, and how to validate the certificate path.
.NET Framework implements the X.509 standard, and all the classes needed to create and manage the certificates are defined in the System.Security.Cryptography.X509Certificates namespace. If you have a certificate with the private key installed locally, you can use it to decrypt data encrypted with the corresponding public key.
To work with certificates programmatically, you can use the X509Certificate2 class.
Certificates use the notion of certificate stores, which are the places where the certificates are securely kept. In .NET stores are implemented in the X509Store class. Windows offers two store locations represented by the StoreLocation enumeration. The possible values are shown in Table 12-10.
Member Name | Description |
CurrentUser | Represents the certificate store used by the current user |
LocalMachine | Represents the certificate store common to all users on the local machine |
Windows offers eight predefined stores represented by the StoreName enumeration. The possible values are shown in Table 12-11.
Member Name | Description |
AddressBook | Certificate store for other users |
AuthRoot | Certificate store for third-party CAs |
CertificateAuthority | Certificate store for intermediate CAs |
Disallowed | Certificate store for revoked certificates |
My | Certificate store for personal certificates |
Root | Certificate store for trusted root CAs |
TrustedPeople | Certificate store for directly trusted people and resources |
TrustedPublisher | Certificate store for directly trusted publishers |
X509Store class offers a string property called Name, which you can use if you want to create your own store.
The following program shows how you can use the X509Store to show the X509Certificates information similar to the one in Figure 12-2.
using System;
using System.Security.Cryptography.X509Certificates;
namespace Encryption {
class CertificateTest {
static void Main() {
X509Store store = new X509Store(StoreName.Root,
StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
Console.WriteLine("Friendly Name Expiration date");
foreach (X509Certificate2 certificate in store.Certificates) {
Console.WriteLine("{0} {1}", certificate.FriendlyName
, certificate.NotAfter);
}
store.Close();
}
}
}
This small program starts by opening in read-only mode the root store from the local machine and then prints the name and the expiration date of all the certificates in there.
Sometimes you might just need to keep data safely without thinking too much about the algorithm used or how it is implemented. For example, in order for an encryption algorithm to be effective, you will need to protect the shared secret for a symmetric algorithm, and the private key for an asymmetric algorithm.
To solve this kind of problems, .NET Framework offers in the System.Security.Cryptography namespace one static class named ProtectedData. This class has two static methods: Protect and Unprotect. As the name implies, the first one is used to encrypt the data, and the second one is used to decrypt the data.
Their signatures are as follows:
public static byte[] Protect(
byte[] userData,
byte[] optionalEntropy,
DataProtectionScope scope
)
public static byte[] Unprotect(
byte[] encryptedData,
byte[] optionalEntropy,
DataProtectionScope scope
)
As you can see, both methods accept three parameters. The first parameter represents the data to by encrypted (userData) or decrypted (encryptedData); the second one is called optionalEntropy, and it is used to increase the complexity of the encrypted data; and the last one is called scope and is of type DataProtectionScope. The scope parameter specifies who can decrypt the data. The values it can take are: DataProtectionScope.CurrentUser and DataProtectionScope.LocalMachine. First, one specifies that only the current user can decrypt the encrypted data, and the second one specifies that any logged-in user on the local machine will be able to decrypt the data.
Protect returns the encrypted data, and Unprotect returns the unencrypted data.
byte[] ProtectString(string data) {
byte[] userData = System.Text.Encoding.Default.GetBytes(data);
byte[] encryptedData = ProtectedData.Protect(userData, null,
DataProtectionScope.CurrentUser);
return encryptedData;
}
Microsoft recommends the following algorithms to be used in the following situations:
You can find a link to more information about the Cryptography Model on MSDN in the “Additional Reading and Resources” section.
using System;
using System.Text;
using System.Security.Cryptography;
namespace EncryptionSamples.Asymmetric
{
public class RSASample
{
public static void Main() {
string keyContainerName = "MyKeyContainer";
string clearText = "This is the data we want to encrypt!";
CspParameters cspParams = new CspParameters();
cspParams.KeyContainerName = keyContainerName;
RSAParameters publicKey;
RSAParameters privateKey;
using (var rsa = new RSACryptoServiceProvider(cspParams)) {
rsa.PersistKeyInCsp = true;
publicKey = rsa.ExportParameters(false);
privateKey = rsa.ExportParameters(true);
rsa.Clear();
}
byte[] encrypted = EncryptUsingRSAParam(clearText, publicKey);
string decrypted = DecryptUsingRSAParam(encrypted, privateKey);
Console.WriteLine("Asymmetric RSA - Using RSA Params");
Console.WriteLine("Encrypted:{0}", Convert.ToBase64String(encrypted));
Console.WriteLine("Decrypted:{0}", decrypted);
Console.WriteLine("Asymmetric RSA - Using Persistent Key Container");
encrypted = EncryptUsingContainer(clearText, keyContainerName);
decrypted = DecryptUsingContainer(encrypted, keyContainerName);
Console.WriteLine("Encrypted:{0}", Convert.ToBase64String(encrypted));
Console.WriteLine("Decrypted:{0}", decrypted);
}
static byte[] EncryptUsingRSAParam(string value, RSAParameters rsaKeyInfo)
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(rsaKeyInfo);
byte[] encodedData = Encoding.Default.GetBytes(value);
byte[] encryptedData = rsa.Encrypt(encodedData, true);
rsa.Clear();
return encryptedData;
}
}
static string DecryptUsingRSAParam(byte[] encryptedData,
RSAParameters rsaKeyInfo) {
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(rsaKeyInfo);
byte[] decryptedData = rsa.Decrypt(encryptedData, true);
string decryptedValue = Encoding.Default.GetString(decryptedData);
rsa.Clear();
return decryptedValue;
}
}
static byte[] EncryptUsingContainer(string value, string containerName)
{
CspParameters cspParams = new CspParameters();
cspParams.KeyContainerName = containerName;
using (var rsa = new RSACryptoServiceProvider(cspParams))
{
byte[] encodedData = System.Text.Encoding.Default.GetBytes(value);
byte[] encryptedData = rsa.Encrypt(encodedData, true);
rsa.Clear();
return encryptedData;
}
}
static string DecryptUsingContainer(byte[] encryptedData, string container)
{
CspParameters cspParams = new CspParameters();
cspParams.KeyContainerName = container;
using (var rsa = new RSACryptoServiceProvider(cspParams))
{
byte[] decryptedData = rsa.Decrypt(encryptedData, true);
string decryptedValue = Encoding.Default.GetString(decryptedData);
rsa.Clear();
return decryptedValue; }
}
}
}
Compiling projects results in executable files, generally known as assemblies. Assemblies are the building blocks of every .NET applications. They are the fundamental unit for deployment, versioning, reuse, and security.
An assembly contains Intermediate Language (IL), metadata, and resources. IL is the result of compiling the source code files into a common language that the Common Language Runtime (CLR) understands. CLR has a component called a Just In Time (JIT) compiler that compiles the IL code into binary code to run it on a specific platform, such as Windows. An assembly can be a single-file assembly or a multifile assembly. Because Visual Studio can generate only single-file assemblies, this book does not cover the multifile assembly, but you can find additional information in the “Additional Reading and Resources” section at the end of this chapter.
An assembly is obtained by compiling a project. The resulting assembly could be a process assemblies (EXE files) or library assemblies (DLL files); the only difference between the two of them is the presence of the main entry point in process assemblies. The main entry point indicates which method should be called when the application is started, aka the Main method.
Because an assembly is the unit of deployment of any .NET application, if you want to split your application in smaller units of deployment to make it more manageable and reusable by different products, you need to split the functionality into different assemblies. Every assembly can then represent a module in your application, which can be updated independent of the other projects. The simplest way to implement this kind of modularization is to create different projects, where one project is one module, and group those into one big solution. To differentiate between different versions of the same assembly, you need to use Assembly Versions.
Software evolves, and the way to mark that is by creating new versions, with every new version adding new features, fixing bugs, or being a complete rewrite of the software, hopefully for the better.
The .NET assembly version is implemented as a string, normally made up of four numeric parts separated by dots: Major, Minor, Build number, and Revision. For instance, the current version of .NET is known as 4.5. This is the marketing version number, or what the customers get to see. Instead, the assemblies that were released with this version have the following version string embedded: 4.0.30319.17929, where 4 represents the Major number, 0 the Minor number, 30319 the Build number, and 17929 the Revision. If you choose to have a version number that is in another format, the compiler generates an error. The recommendation is to use the number format to give the assembly version a meaning.
You can set the version of your assembly in two ways. First is via your project settings. Right-click your project settings and choose Properties. On the Assembly property page, press the Assembly Information button. A dialog similar to the one in Figure 12-4 appears. The four Assembly Version fields represent the aforementioned parts of the assembly version.
The second way to set the version is to go directly and edit the AssemblyInfo.cs file. To find the file, you have to click the project for which you want to change the version, and then expand the property node, as shown in Figure 12-5.
At the end of the file, you can find a line resembling the following code:
[assembly: AssemblyVersion("2.1.42.15")].
As explained in Chapter 8, “Creating and Using Types with Reflection, Custom Attributes, the CodeDOM, and Lambda Expressions,” this is an attribute implemented in the AssemblyVersionAttributeapplied to the assembly that specifies which version the assembly should have.
The Assembly Version is ignored by CLR unless it is used together with the strong names. Strong names are covered in the next section.
One of the major problems developers were facing prior to .NET was the DLL hell. DLL stands for dynamic linked library, and it was a way to share code and optimize the space an application takes on the disk. The problem that the DLL hell refers to could appear when different applications used the same library. If they were compiled against different version of the same library, the one that was installed last would override the existing DLL. If there were compatibility issues between different versions of the same assembly, one application started to act erratic, or even refused to start. .NET solved this problem by introducing a new concept called side-by-side versioning. For this to work, you must make a distinction between different versions of the same assembly, while assuring that the two different versions are coming from the same source.
In .NET the name of the assembly consists of four parts:
The friendly name is what you previously called the library name. In the normal situation, this is the name of the assembly file without the extension, so if your assembly file were called ProgrammingCSharp.dll, then the assembly-friendly name would be ProgrammingCSharp.
The version is the Assembly Version discussed in the previous section.
The culture represents the culture of the assembly. The culture is used when you want to localize your application for different markets. To localize your application, you create one assembly containing the code and one assembly for each region you want your application to be localized for. The localized assemblies contain only resources for that specific region such as translated strings or specific images. The executable assembly always has the culture set to neutral. If you try to set it to something else, an error will be generated by the complier. Globalization and localization are outside the scope of this exam, but if you want to know more about this, please see the “Additional Reading and Resources” section for a link to the MSDN list of articles about this subject.
For an assembly to have a strong name, it needs to be signed with a public/private pair of keys using the techniques discussed in the previous section. The private key is used to digitally sign the assembly, whereas the public key is used to verify the signature. The Public Key Token is a 64-bit hash of the assembly public key. The reason that the .NET uses a hash of the signature instead of the signature itself is to save space because the signature is much bigger than 64 bits. An assembly that is not digitally signed can have the Public Key Token set to null.
As mentioned earlier, to sign an assembly, you need a pair of public/private keys. You can generate those keys in two ways. The first way is to create the file via Visual Studio. To do that, you right-click the project and then choose Properties. On the properties page, choose the Signing tab. There, check the Sign the Assembly check box, as shown in Figure 12-7.
When you do that, a dialog similar to the one shown in Figure 12-8 appears.
As you can see, there you can specify the name of the key file, a password that you can use to protect the private key in the file, and the algorithm to be used for the signature. The choices for the algorithm are sha256RSA and sha1RSA. After you create the file, it will be added to your project. When your project is opened by another user, or by you on another machine, you must to introduce the password you just set in the previous step.
The second way to generate a key is to use the sn.exe program. To do that, you need to open the Developer Command Prompt for VS2012. From the command line, run the following:
sn.exe -k myFile.snk
The command output should look similar to the one in Figure 12-9.
After you generate the file, instead of choosing <New…> in Visual Studio on the Signing tab, you need to choose <Browse…> and choose the newly created file. The downside of using sn.exe is that you cannot specify the password, but sn.exe is used in the delay sign scenario, to extract the public key from the file, or to sign the assembly before the deployment. You can find more information about sn.exe in the “Additional Reading and Resources” section.
When an assembly needs to be digitally signed, the compiler signs the assembly using the private key and embeds the public key in the assembly for later verification by other assemblies that refer to it. The next step is to hash the public key to create the Public Key Token, and embed that one as well into the assembly.
In conclusion, the assembly name is not only the filename or the friendly name. The complete name of an assembly is known as the Fully Qualified Name (FQN). For instance the FQN of the System assembly in .NET 4.5 is
System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Strong naming would have only little value without the power of running side-by-side different versions of the same assembly. Consider the following scenario.
In your organization there are two teams working on two different products, but both of them need to use some common functionality. As mentioned earlier, the best way to deal with this kind of situation is to move the common code into a separate project, creating a separate assembly that can be used by both teams. Everything works fine; they deploy their products, and there is only one copy of the common assembly per machine, instead of having one per product. After a while the first team might need some additional functionality to be added into the common assembly, but the second team is “not there yet.” Without the side-by-side versioning, you must keep a copy of the assembly private for every installation. With two products this might be an acceptable solution, but if you have more products, or if you are a third-party vendor, then it will become much harder to maintain this kind of solution.
Side-by-side versioning works only with strong name assemblies because it requires them to be deployed into the Global Assembly Cache (GAC). GAC is covered in the next section. But for now, see how side-by-side versioning works.
When you add a reference to an assembly, information about the referred assembly is added to the manifest file of the assembly. The manifest is embedded into the assembly as part of the metadata. Inside the manifest file every referenced assembly is represented by a block resembling the following code:
// Metadata version: v4.0.30319
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .zV.4..
.ver 4:0:0:0
}
.assembly extern CommonFunctionality
{
.ver 1:0:0:0
}
.assembly 'Programming CSharp Assembly'
{ … }
The first .assembly extern line represents the reference to the mscorlib assembly, which is referenced by default by all .NET applications because this one contains all the definitions for all the base data types in .NET. The version of the assembly is 4.0.0.0 and the Public Key Token is (B7 7A 5C 56 19 34 E0 89). The second .assembly extern represents a reference to an assembly called CommonFunctionality that has version 1.0.0.0 and is not signed. If the second assembly would have been signed as well, the manifest information would have been something like this:
// Metadata version: v4.0.30319
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .zV.4..
.ver 4:0:0:0
}
.assembly extern CommonFunctionality
{
.publickeytoken = (48 27 85 37 58 E3 97 63 ) // H'.7X..c
.ver 1:0:0:0
}
.assembly 'Programming CSharp Assembly'
{ …}
This information is shown using the Intermediate Language Disassembler (ildasm.exe) application. To see that, open the Developer Command Prompt for VS2012 and go the output folder of your assembly (normally <Your Project Folder>inDebug). From there type ildasm.exe <Your assembly name>.exe. The command output should look similar with the one in Figure 12-10.
This opens the IL DASM application, as shown in Figure 12-11.
From there you can double-click the MANIFEST node to open the manifest file, as shown in Figure 12-12.
Now when you try to run the application, the run time tries to locate the right assemblies for you.
If an assembly is not signed, CLR looks into the local folder of the application to try to find an assembly based only on the assembly name and assembly filename, ignoring the assembly version.
If the assembly is signed, CLR first tries to see if there are any policies specified for that particular assembly that can instruct CLR to use a different version. This process of replacing an assembly without requiring to recompile the assemblies that use it is called binding redirection. After the version is established, CLR first looks in the GAC for that particular version of the assembly, and if it is not there, it looks in the local folder of the application to try to find the right assembly. When the CLR finds the assembly, it verifies the digital signature of the assembly to ensure that the assembly wasn’t tampered with. If the CLR finds the assembly but doesn’t find the right version for it or the signature doesn’t match, it throws a System.IO.FileLoadException. If the CLR cannot find the assembly, a System.IO.FileNotFoundException is thrown.
How CLR does probing on the local folders is outside the scope of this exam, but you can find a link on the “Additional Reading and Resources” section.
Member Name | Description |
None | Not specified or unknown |
MSIL | Processor independent |
X86 | A 32-bit Intel processor |
IA64 | A 64-bit Intel processor only |
Amd64 | A 64-bit AMD processor only |
Arm | An ARM processor |
System, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL
As mentioned earlier, only signed assemblies can be placed in the GAC, but what is the GAC?
If your application is a non-.NET application and uses dynamic link libraries, it looks for them either on the folder that the application was installed on or on the folders specified in the PATH environment variable. If you want to see the value of the PATH environment variable, open the Developer Command Prompt for VS2012 and run the following commend:
set PATH
This outputs something similar with the text shown in Figure 12-13.
Instead, .NET uses the concept of GAC, which is an assembly repository that acts as a cache for shared libraries. The location of the GAC is <Your Windows Installation Folder>assembly. If you want to see which assemblies are in your GAC, you can open Windows Explorer and navigate to this folder, and you see something resembling Figure 12-14.
As you can see, the list shows all the aforementioned attributes of an assembly: Assembly Name, Version, Culture, Public Key Token, and Processor Architecture. The empty Culture means that this is the neutral culture, whereas the empty Processor Architecture means NONE for the processor architecture. There is no empty Public Key Token because only strong named (digitally signed) assemblies can be deployed on the GAC.
Before talking about how to add assemblies to GAC, you’ll first explore the advantages of using the GAC.
First, two advantages already mentioned are side-by-side versioning and sharing of assemblies. If an assembly is already loaded in memory, the CLR does not load it again.
Another advantage is that the assembly signature is verified before it is installed on the GAC, so when the assembly will be loaded by the CLR when it is executed, it skips verification improving the startup time of your application.
Last is the possibility to precompile the assemblies, so they won’t need to be compiled by the JIT compiler every time you load them, as discussed at the beginning of this section. This can speed up the startup process even more. To do that you must run a utility called ngen.exe (Native Image Generator), which is outside the scope of this book, but as always you can find more information in the “Additional Reading and Resources” section.
You can install an assembly in the GAC in two ways.
First, use an installer that can do this. This is the preferred way. By using the installer you ensure that the installation is atomic, meaning that it either succeeds to install all the components or none at all, and it gives you the possibility to uninstall it later in the same manner. When an assembly is installed this way, the installer adds the assembly if is not there, or if it is there, it increases the reference count only, making sure that the assembly won’t be uninstalled if it is still used when you uninstall the application.
The second way is to use the utility called gacutil.exe, which can be run as follows:
gacutil.exe [options] [assemblyName | assemblyPath | assemblyListFile]
Table 12-13 shows the most common parameters for the gacutil.exe. For a complete reference, refer to the “Additional Reading and Resources” section.
Option | Description |
/i | Adds an assembly to the GAC. |
/u | Removes an assembly from the GAC. |
/l [assemblyName] | Lists all the assemblies in the GAC. Specifying the assemblyName parameter will list only the assemblies matching that name. |
/r | Traces references to an assembly by increasing a counter on every install and decreasing the counter on uninstall. Specify this option with the /i, /il, /u, or /ul options. If an assembly is traced, it will be removed from GAC only when the counter is 0. |
In this chapter you learned how to encrypt sensitive data in your application using both symmetric and asymmetric algorithms. You looked at some of the existing algorithms, how you can manage and create certificates, and how to manage the encryption keys. Then you looked at how to hash data to ensure that the information is not modified and you ended by looking at how you can encrypt streams of data.
In this chapter you learned as well about assemblies, what they are, and how you can create strong-named assemblies to use several side-by-side versions. You then looked at the options available to add an assembly to the GAC, and how you can create a WINMD assembly.
The following questions are similar to the types of questions you will find on Exam 70-483. Read each question carefully and select the answer or answers that represent the best solution to the problem. You can find the answers in Appendix A, “Answers to Chapter Test Questions.”
Here are some additional useful resources to help you understand the topics presented in this chapter:
Cryptography Model http://msdn.microsoft.com/en-us/library/0ss79b2x.aspx
Makecert.exe (Certificate Creation Tool)http://msdn.microsoft.com/en-us/library/bfsktky3.aspx
Working with Assemblies http://msdn.microsoft.com/en-us/library/8wxf689z.aspx
Globalization and Localization http://msdn.microsoft.com/en-us/library/hh965328.aspx
Delay signing of an assembly http://msdn.microsoft.com/en-us/library/t07a3dye.aspx
Ngen.exe (Native Image Generator) http://msdn.microsoft.com/en-us/library/6t9t5wcf(v=vs.110).aspx
Sn.exe (Strong Name Tool) http://msdn.microsoft.com/en-us/library/k5b5tt23(v=vs.110).aspx
How CLR Locates Assemblies http://msdn.microsoft.com/en-us/library/yx7xezcf.aspx
Gacutil.exe (Global Assembly Cache Tool) http://msdn.microsoft.com/en-us/library/ex0ss12c.aspx
This cheat sheet is designed as a way for you to quickly study the key points of this chapter.
Choosing an encryption algorithm
Symmetric encryption
Asymmetric encryption
Hashing
Key management
Assembly version
Strong name
GAC
assembly An assembly is the unit of reuse, deployment, versioning, and security.
asymmetric encryption (public key) A cryptographic algorithm that uses two complementary keys, one for encryption and one for decryption. Data encrypted with the public key can only be decrypted using the private key.
Certificate Authority (CA) An entity that issues digital certificates.
Certificate Revocation List (CRL) A list of digital certificates that has been revoked for various reasons. You shouldn’t use a certificate if it is revoked.
certificate stores A special storage location on your computer, used to store encryption certificates.
Common Language Runtime (CLR) CLR is the component of .NET Framework responsible for running .NET applications and managing their running environment.
cryptography The practice and study of techniques for secure communication.
decryption The process of decoding previously encrypted data so that it can be used by your application.
encryption The process of encoding data so that it cannot be read by an unauthorized person.
Global Assembly Cache (GAC) GAC is a machine-wide code cache.
hash bucket A data structure that holds items that share the same hash value.
hashing Used to map data structures of variable length, to fixed size data structures. Hashing the same data using the same algorithm will always yield the same hash value.
initialization vector (IV) A data array used by the encryption algorithms to encrypt the first data block. The IV doesn’t need to be kept secret.
Intermediate Language (IL) The result of compiling a .NET application from source code.
Just In Time compiler (JIT) A component of the .NET that transforms the IL into binary code that can be run on the target platform.
Message Authentication Code (MAC) A family of cryptographic algorithms used to provide data integrity and authenticity.
private key The public and private keys are a pair of complementary keys used together in the asymmetric encryption. Data encrypted with the private key can only be decrypted using the public key, and data encrypted with the public key can only be decrypted using the private key.
public key See private key.
Public Key Infrastructure (PKI) The infrastructure needed to handle digital certificates.
Secured Hash Algorithm (SHA) A family of cryptographic algorithms used to calculate hashes published by NIST.
Secure Socket Layer (SSL) A cryptographic protocol used for secure communication over the Internet.
symmetric encryption (shared secret) A cryptographic algorithm that uses the same key for both encryption and decryption of data.
Transport Layer Security (TLS) A cryptographic protocol used for secure communication over the Internet, the successor of SSL.
18.117.99.71