Implementing the Public Key Infrastructure
Enough background! Let's get started on your infrastructure. First, create a new VB.NET Class Library project. Name the Project PKInfrastructure. In the solution explorer, right click the project name and select properties. On the Common PropertiesGeneral settings, specify PKInfrastructure as the assembly name and also as the root namespace. Also in the solution explorer right click on the Class1.vb
file and delete it.
The Key, KeyPair, and KeyRing classes are fairly straightforward. Right click on the project name in the solution explorer, select Add, then Add Class. Name the class "Key." When the new file (Key.vb) is opened for editing, past the code from Listing 1. Repeat this process to add each of the other classes to the project: KeyPair (Listing 2), KeyRing (Listing 3),CryptographyFunctions (Listing 4) and PublicKeyGenerator (Listing 5).
The CryptograhyFunctions class (CryptographyFunctions.vbListing 4) is where almost all of the action takes place. This class encapsulates and abstracts the functionalities you need from the .NET framework, and handles the four cryptographic functions identified earlier (the sign, encrypt, decrypt, and authenticate functions). However, the creation of new key pairs is delegated to the PublicKeyGenerator class (PublicKeyGenerator.vbListing 5).
Generate a Public/Private KeyPair
System.Security.Cryptography.RSACryptoServiceProvider (RSA) is the workhorse of public key cryptography in the .NET framework. By default, this class creates a new public/private key set when it is instantiated. It also attempts to use a "key store" for the user account of the current user. Override these default behaviors by passing a customized parameters object during instantiation. The GetRSA function (Listing 5) abstracts this and returns an instance of the RSA class that is configured to use a machine key store rather than one based on the current user account.
The RSA class provides access to the public and private keys through the serialization functions ToXmlString and FromXmlString. The ToXmlString function takes a Boolean parameter which indicates whether private key information should be included. The MakeKeyPair function calls this method with false to get the public key XML string and again with true to get the private key XML. In each case, the XML is loaded into a Key instance of the new KeyPair object. This KeyPair object is then returned from the MakeKeyPair function.
Sign and Encrypt a Message
Many of the cryptographic functions use an RSA instance. Therefore, when the CryptographicFunctions class instantiates, it instantiates an RSA object and stores it in a member variable (m_objRSA) where it is accessible to all of the functions of the class.
The SignAndEncrypt function (Listing 4) takes a KeyPair object containing the sender's private key and the recipients public key and a plain text string to sign and encrypt. The function calls Sign, passing the KeyPair and plaintext. The resulting signed text is then encrypted by the Encrypt function.
The Sign function initializes the m_objRSA object with the private key information and calls the SignData method to get the signature. However, the RSA's SignData method does not operate on string data. Therefore, the string text is converted into an array of bytes that are then passed into the SignData method. The SignData method returns an array of bytes which is then converted back into a string. Since most of the .NET public key cryptography functions use byte arrays rather than strings, these conversions are handled in helper functions (StringAsByteArray and ByteArrayAsString). The RSA class' SignData method requires a HashAlgorithm object to compute the hash bytes for the message. Pass in a new instance of the default hash algorithm from System.Security.Cryptography.HashAlgorithm.Create().
The Encrypt function takes a KeyPair object and the plain text to be encrypted. The m_objRSA object is initializedthis time with the public keyfor encryption. This is where it gets a little tricky. The RSA class supports encryption through the Encrypt method. However, the Encrypt function operates on an array of bytes. It modifies each byte of input using a corresponding portion of the public key. If the input array has fewer bytes and doesnt use the entire key, padding is used so that the output is always the same length and depends only on the size of the key. If the input is larger than the public key, the encryption fails with an error indicating "bad data" or "bad length." The work-around for this conundrum breaks the plain text into blocks to ensure that each block is not too long to be encrypted. Convert these encrypted blocks back into strings (from byte arrays) and concatenate them together to produce the final cipher text.
There is one further complicating twist. The type of padding employed depends on the operating systemand a minimum amount of padding may be applied to each block. This is described on Microsoft's MSDN site here. You need to determine how much is the minimum amount of padding that will be applied to each encrypted block and the key size in order to determine the maximum block size. Windows XP and later support 'OAEP padding (PKCS#1 v2)' in which the minimum amount of padding applied to each encrypted block depends upon the size of the key and the size of the hash (message digest). Windows 2000 or later with the high encryption pack support 'PKCS#1 v1.5' padding in which the minimum amount of padding is 11 bytes. Previous versions of Windows and Windows 2000 without the high encryption pack may use no padding.
If you use the default key size of 1024 bits (128 bytes), PKCS#1 v1.5 padding (with a minimum of 11 bytes added to each block), and 1 byte per string character, then the maximum string length you can safely encode is 117 characters at a time. The helper function GetModulusSize returns the key size in bytes. The encryption algorithm subtracts 11 from this to get the maximum number of characters and repeatedly calls the EncodeBlock function with the next group of characters.
The EncodeBlock function simply calls the encode method of the m_objRSA object. The string is converted into an array of bytes for encryption and the encrypted bytes (with padding) are converted back into a string. The encrypted block strings are accumulated in the Encrypt function and returned.