The CryptoAPI seems quite complicated at first glance due to the variety and complexity of supported security keys. But, by just using a handful of the simpler routines and default parameters you can do some very useful things, such as hashing data and encrypting and decrypting data. In this months column, I will demonstrate using the Crypto APIs to encrypt and decrypt a text file.
Securing data typically involves privacy, authentication, and integrity. Ensuring privacy typically involves encryption and ensures that only the intended recipient can actually see the real data. Typically, special keys are used to encrypt and decrypt data. The simplest algorithm (symmetric) involves using the same key (session key) for both encryption and decryption. Another more complicated but secure algorithm (asymmetric) uses different keys (public/private key pair) for encryption and decryption. In either case, the complexity often involves methods for securing the keys, themselves. To provide for privacy, the CryptoAPI provides basic cryptographic functions, simplified and low-level message functions, and functions for encrypting and digitally signing data.
Authentication involves verifying a persons identify and usually involves credentials of some sort. Digital certificates are a popular means of secure credential. To provide authentication support, the CryptoAPI includes functions to encrypt, decrypt, and store certificates.
Integrity refers to ensuring that data sent from a sender to a recipient has not been modified (either accidentally or intentionally). This is often done by a combination of hashing the data and sending the hash with the digital certificate. To ensure data integrity, the CryptoAPI includes functions for hashing data and signing data with digital certificates.
Cryptographic Server Providers
A separate module known as a CSP (Cryptographic Server Provider) always performs the actual cryptographic operations. A CSP is a DLL that exports interfaces for performing encryption, decryption, and generation and storage of keys. Each CSP specifies its key exchange algorithm, digital signature algorithm, exported key format (also referred to as a BLOB), digital signature format, session key derivation scheme, key length, and other default modes.
Each CSP provides a key database that it uses to store persistent cryptographic keys. Each key database can contain one or more key containers. Generally, a default key container is created for each user. This key container takes the users logon name as its own name, which is then used by any number of applications. It is also possible for an application to create its own key container (and key pairs), which it usually names after itself. Typically, there are two public or private key pairs for each user; one is used to encrypt session keys (key exchange pair) and the other is used to create digital signatures (signature pair).
CSPs have both a name and a type. The name is unique, but there can be more than one CSP of the same type. For example, one of the CSPs currently shipped with most Microsoft operating systems is the Microsoft Base Cryptographic Provider. Its name is MS_DEV_PROV and its type is PROV_RSA_FULL. The following predefined provider types are available by default: PROV_RSA_FULL, PROV_RSA_AES, PROV_RSA_SIG, PROV_RSA_SCHANNEL, PROV_DSS, PROV_DSS_DH, PROV_DH_SCHANNEL, PROV_FORTEZZA, PROV_MS_EXCHANGE, and PROV_SSL.
It is possible to write your own custom CSP. Table 1 lists the CSPs currently available from Microsoft.
The CMyCrypto Class
I wrote a simple class called CMyCrypto to wrap a handful of the CryptoAPI routines and provide a simple Encrypt and Decrypt method for encrypting and decrypting raw data. The include that contains the definition of the class is in Listing 1 (crypto.h) and the source file that contains the implementation of the methods is in Listing 2 (crypto.cpp).
The bulk of the work involved in performing encryption and decryption occurs in the constructor where I create the key that will be used for both encryption and decryption. The constructor for CMyCrypto takes two parameters: a string containing the password used to encrypt the data and an ALG_ID (type defined as an unsigned int) value describing which algorithm to use for the encryption. Ill describe both of these parameters in more detail later.
When dealing with any of the CryptoAPI routines, you invariably need to call CryptAcquireContext() first to get a handle to a CSP. This handle is then passed as a parameter to most of the other CryptoAPI routines. When calling CryptAcquireContext(), you specify which CSP you wish to use and which key container (a new key container, a specific existing key container, or the default key container for the logged on user). In my constructor, I specify the Microsoft Base Cryptographic Provider (its name is MS_DEV_PROV and its type is PROV_RSA_FULL) and the default key container for the logged on user. The Microsoft Base Cryptographic Provider is available with both version 1.0 and 2.0 of the CryptoAPI. It supports data encryption and digital signatures. It currently ships with Windows NT/2000/XP and Windows 95/98/Me as well as with Internet Explorer 3.0 and later. If your cryptographic needs are fairly basic, this is a good default choice that is very prevalent.
You can optionally specify just a provider type and then CryptoAcquireContext() will first search for a provider of that type in the list of default providers for the logged on user and then in the list of default providers for the current machine. CryptoAcquireContext() returns a handle to the specified CSP which allows access to other CSP functions and the specified key container within the CSP.
While there are several ways to encrypt and decrypt data using various types of security keys, for simplicity, I chose to limit my code to using only symmetric algorithms. Symmetric algorithms use a session key to encrypt and decrypt data. Session keys are created by specifying a hash of some kind of unique data to create a key that is then used to encrypt and decrypt the data. In this case, the unique data is the password string that is passed to the constructor as the first parameter.
To hash data using the CryptoAPI routines, you must first call CryptCreateHash() to create a hash object. Like all CryptoAPI routines, CryptCreateHash() takes a handle to a CSP as an input parameter. You can also specify what hashing algorithm you wish to use. For simplicity, I did not want the users of CMyCrypto to have to be aware of the fact that hashing is going on in the background, so I did not expose the hashing algorithm as a settable parameter. Instead, I always use the MD5 hashing algorithm to hash the password data.
Now that I have a handle to a hash object, I can hash the password string usingCryptHashData(). Note that you can call CryptHashData() multiple times if you have a large amount of data that needs to be hashed. You can also call CryptGetHashParam() to retrieve the hashed data, making these routines useful as general purpose hashing functions.
To create a session key, I called CryptDeriveKey(), which takes a handle to the hash object and extracts the hashing data itself (avoiding the necessity of calling CryptGetHashParam()). In addition to handles to the CSP and the hashing object, CryptDeriveKey() also takes an ALG_ID parameter and a flags parameter. The ALG_ID parameter is the second parameter passed to the constructor and is passed directly to CryptDeriveKey(). The flags parameter is a bit more complicated. The lower 16-bits of this parameter are comprised of a predefined set of flags. For example, to allow a session key to be used outside the current session, the CRYPT_EXPORTABLE flag must be specified, otherwise the session key is only usable by the application that created it. The upper 16-bits of the flags parameter specify the key size. Since I always use the Microsoft Base Cryptographic provider, I can hard code this value to 0x0028 for the upper 16-bits, since this provider always generates 40-bit session keys. You may wish to modify this in your own applications. Once the key has been created, the hash object is no longer needed, so I free it by calling CryptDestroyHash() . The generated key is saved in a private member variable for use later by the Encrypt and Decrypt methods.
The destructor for the CMyCrypto class frees the handle to the key by calling CryptDestroyKey() and frees the handle to the CSP by calling CryptReleaseContext().
The Encrypt and Decrypt Methods
The Encrypt method takes four parameters (a pointer to a buffer, a pointer to a DWORD containing the size of the data stored in the buffer, a DWORD containing the allocated buffer size, and a Boolean value that indicates whether this is the last block of data to be encrypted) that are passed directly to the CryptoAPI CryptEncrypt() routine. CryptEncrypt() also takes as a parameter, a handle to the key to use for encryption process.
Because CryptEncrypt() writes the encrypted data back into the original buffer, its important to know the data size as well as the buffer size. If the encrypted data requires more buffer space than the plain text data, the CryptEncrypt() can make use of any available buffer space. If a large amount of data needs to be encrypted, CryptEncrypt() can be called multiple times. The third BOOL parameter is set to TRUE if this is the last buffer of data to be encrypted (this gives CryptEncrypt() an opportunity to do any final processing and to reset its state for a new encryption request). Note that CryptEncrypt() takes an optional hash object as a parameter; this allows you to both hash an encrypt the data at the same time.
The Decrypt method behaves very similarly to the Encrypt method. It takes a similar set of parameters and passes them directly to the CryptDecrypt() routine.
All the definitions you will need for using the CryptoAPI routines are found in wincrypt.h. You will need to include the crypt32.lib library when building the sample code.
The Sample Calling Program
To demonstrate using the CMyCrypto class, I wrote the sample program in listing 3 (main.cpp). This program uses the following command line syntax.
To encrypt the plain text contents of <file1> using <password> for the password and save the encrypted data to <file2>:
Crypto -e <password> <file1> <file2>
To decrypt the contents of <file1> using <password> for the password and save the decrypted (plain text) data to <file2>:
Crypto -d <password> <file1> <file2>
After validating the command line parameters, the sample program opens the two files and allocates a buffer to use for reading/writing and encrypting/decrypting the data. Then I instantiate an instance of the CMyCrypto class, passing the second command line parameter as the password and choosing the RC4 stream encryption algorithm (a common symmetric algorithm). If -e was specified as the first command line parameter, I read the file and pass the buffer to the Encrypt method. If -d was specified, I pass the buffer to the Decrypt method. In either case, the buffer that is returned from the method is saved to the second file. If you encrypt a file and then decrypt it with the same password, you should end up with the exact same file. Note that passwords are case sensitive.
Just the Start
We have only just scratched the surface on the CryptoAPI routines and I have used default values to hide some of the inherent complexity of working with cryptographic functionality. My intent was to demonstrate that it is fairly easy to use a subset of the CryptoAPI features to perform simple tasks such as encrypting/decrypting and hashing data.
Paula Tomlinson has been developing Windows, Windows CE, and Windows NT based applications and device drivers for 13 years. In her spare time, Paula enjoys flying N422HJ, a 1951 Ryan Navion. The opinions expressed here are hers alone. Paula can be reached at [email protected].