Introduction
This article is an introduction to Windows CAPI programming. I was some what surprised to learn that CodeProject's Security section did not include an article examining CAPI with a beginner in mind. I was also a bit confounded that there was no 'drop in' C++ AES wrapper for CAPI. This article will attempt to address both issues, especially for the beginner.
The article will develop a simple C++ wrapper called WinAES
which will allow us to encrypt and decrypt data with AES in CBC mode (and PKCS #5 padding) using the Windows Cryptographic API. The class will demonstrate how to import existing key material into a container using CryptImportKey
, rather than using CryptGenKey
or CryptDeriveKey
as exemplified in MSDN.
There are alternatives to using CAPI. For example, Microsoft offers the .NET Security APIs and NG, while third parties offer libraries such as Java, OpenSSL, Crypto++ (Wei Dai), and Cryptlib (Peter Guttman). If using the .NET APIs and you require FIPS 140 conformance, be sure to check that the .NET class is a wrapper for a CAPI call and not a managed implementation. The CAPI implementations are FIPS certified, while managed implementations are not. Some - but not all - of the System.Security.Cryptography
classes call into CAPI. For example, the SHA1CryptoServiceProvider
class is FIPS compliant because it calls into CAPI, while the SHA1
class is not. For a complete list of the 47 certified modules, see NIST's FIPS 140 Vendor List.
While this is a gentle yet thorough introduction, the use of encryption alone is usually not adequate for an application. For a discussion of the shortcomings of encryption alone, see Authenticated Encryption. For a drop in WinAES replacement which provides both Encryption and Authentication, see WinAESwithHMAC. As stated earlier, this is a beginner's article. So, intermediate and advanced readers will be sufficiently bored with the material.
Background
AES is the Advanced Encryption Standard. The algorithm was developed by Joan Daemen and Vincent Rijmen. AES is a 128 bit block cipher which can use 128, 192, and 256 bit keys. Because the key size varies but the block size is fixed, it is not uncommon to encounter AES-128, AES-192, and AES-256 in discussions of AES.
AES is the latest block cipher approved for US government use by NIST. The algorithm is also approved for use by NESSIE (a European standards organization for cryptography) and ISO/IEC (a world standards organization).
Windows CAPI
The forward face that programmers see when practicing secure programming is CAPI, or the Cryptographic APIs. Below the surface is a very flexible architecture that allows for extensibility through a SSPI, or the Security Support Provider Interface. Technet has a detailed examination of the architecture at The Security Support Provider Interface.
The concrete implementation of SSPI is a SSP or Security Support Provider. The SSP offers a Security Package (SP) to the application through a DLL. The SP handles operations such as context management, credential management, and authentication between security protocols (examples of protocols include RPC, NTLM, and Kerberos). A Cryptographic Service Provider (CSP) is one facet - or one view - into an SSP.
From the discussion above, it should be evident why libraries such as Crypto++ and OpenSSL are generally easier to use than CAPI - they have fewer features, and don't conform to any particular implementation interface. For example, neither Crypto++ nor OpenSSL offer key stores for any type of key management, while a CSP must. Java, on the other hand, does offer key management, and is a bit more difficult to use.
Service Providers
When we use Microsoft's AES implementation, we use the services of a DLL which offers the AES algorithm and conforms to the SSPI specification. The dynamic link libraries are not loaded directly. Instead, we indirectly specify the DLL when we call CryptAcquireContext
through pszProvider
and dwProvType
. This begs the question, How do we know which CSP to request or use? The simple answer is that we search the list of Microsoft Cryptographic Service Providers. Below, we see the classic providers such as Base, Strong, and Enhanced, in addition to AES and DSS.
|
Figure 1: Microsoft Cryptographic Service Providers
|
The AES Provider Algorithms show us that there is support for AES-128, AES-192, and AES-256, as shown in Figure 2.
|
Figure 2: AES Cryptographic Service Provider Algorithms
|
However, the Microsoft Enhanced Cryptographic Provider may also support the AES algorithm. But, when we visit the supported algorithms page (Figure 3 below), we see that AES is not supported by this package.
|
Figure 3: Microsoft Enhanced Cryptographic Provider Algorithms
|
It appears that we are not simply wrapping AES, or CAPI's CryptEncrypt
and CryptDecrypt
- we are wrapping Service Providers. If we want to implement Authenticated Encryption using AES and a HMAC, we could wrap two providers in a single object: the AES Provider (AES) and the Enhanced Provider (HMAC). Strictly speaking, a second provider is not necessary since the AES provider offers both algorithms.
Available Service Providers
Microsoft's CAPI allows us to enumerate available providers at runtime. But for this article, we can look in the Registry under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Defaults\Provider to examine what is available to us. Figure 4 shows a Windows XP installation.
|
Figure 4: Installed Cryptographic Service Providers
|
One of the most interesting providers listed above is the Intel Hardware Cryptographic Service Provider. The Intel provider is not installed by the Operating System. However, it is a redistributable, and is available for download with this article. The redistributable includes icsp4ms.h, which has the following definitions:
#define PROV_INTEL_SEC 22
#define INTEL_DEF_PROV_A "Intel Hardware Cryptographic Service Provider"
#define INTEL_DEF_PROV_W L"Intel Hardware Cryptographic Service Provider"
#ifdef UNICODE
#define INTEL_DEF_PROV INTEL_DEF_PROV_W
#else
#define INTEL_DEF_PROV INTEL_DEF_PROV_A
#endif
Intel's CSP responds to five CAPI function calls: CryptAcquireContext
, CryptReleaseContext
, CryptGetProvParam
, CryptSetProvider
, and CryptGenRandom
. The code to use the provider is as follows. If the hardware generator is not available, the thread's last error is set to ERROR_DEV_NOT_EXIST
.
If(!CryptAcquireContext(&hProvider, NULL, INTEL_DEF_PROV, PROV_INTEL_SEC, 0))
{
...
}
if(!CryptGenRandom(hProv, randomLength, (BYTE*)&randomNumber))
{
...
}
When using Intel's generator, be sure to read AccessDocumentation.doc provided in the redistributable.
Also of interest may be Intel's Security Driver, which is available for download from http://developer.intel.com/design/software/drivers/platform/security.htm. The Security Driver provides access to the hardware generator in selected chipsets such as Intel's 810 Chipset Family, 815 Chipset Family, Intel 830 Chipset Family, and the 845G Chipset Family.
WinAES
With the administravia completed, we can turn our attention towards the C++ wrapper. When working with CAPI, my two complaints are:
- behavior among providers is not consistent, and
- it is hard to tell where something is amiss when all we receive is
ERROR_INVALID_PARAMETER
from CryptEncrypt
or CryptDecrypt
.
WinAES
will address both by thoroughly validating its parameters and return values. It will be difficult to call CryptEncrypt
or CryptDecrypt
with a configuration which can fail (or exhibit different behavior among DLLs).
If you have read any of John Robbin's Debugging Applications, you will be familiar with massive asserting. If you have not, you are in for quite a treat when something goes wrong. Since this is a beginner article, you should buy John's book. It is amazing what the Visual Studio debugger can do in the hands of someone who is proficient, and the book will help to develop those skills.
There are 55 asserts in the class code. Nearly everything is validated - from incoming parameters to function return values. We will immediately know when something goes wrong. They say that the best code is code that you don't have to write. They are right, but they forgot to mention the second best code - code that debugs itself. Asserts are what we use to create self-debugging code.
WinAESException
WinAES
uses WinAESException
which is derived from the standard exception. Internally, the class uses it frequently (I'm told a goto
is bad style, so I have to disguise it for the purists). As required, the WinAESException
is rethrown in a function, or false
is returned from a function. Which behavior we get is described below.
Since we internally catch a WinAESException
, we have exception handling in place. However, we never catch "..." or anything else we are not prepared to handle. This is good form, and we adhere to it.
Construction
WinAES( const wchar_t* lpwszContainer=NULL, int nFlags=DEFAULT_FLAGS )
The constructor takes both a container name and flags. If we don't specify a container name, the object will use "Temporary - OK to Delete". This makes sense since we don't want to pollute the CSP's default container with unnecessary test keys.
lpszContainer
allows us to specify a container if we prefer other than the default CSP container (NULL
uses the default). Since this is a C++ object, the object must throw on errors. However, at other times, I prefer an iterative approach: I want the method to return false
on error. We should also be able to specify whether to delete the container (if not NULL
). nFlags
controls the behavior, with DELETE_CONTAINER=1
, THROW_EXCEPTION=2
. DEFAULT_FLAGS
only deletes the container.
Finally, the call to CryptAcquireContext
is made during construction. MSDN shows us that we should call CryptAcquireContext
with a provider name and type, and if it fails, we call it again with CRYPT_NEWKEYSET
. In addition, there are slightly different providers for XP and non-XP machines. We could dynamically determine the OS version and call either MS_ENH_RSA_AES_PROV
or MS_ENH_RSA_AES_PROV_XP
, but it is easier to try both. My personal preference is to do as much work as possible at compile time. The code is very portable with little maintenance (it also works on Windows CE). Finally, this also helps to stay out of deep nesting of if
statements (which, in my mind's eye, is not elegant).
typedef struct PROV_PARAMS_T
{
const WCHAR* lpwsz;
DWORD dwType;
DWORD dwFlags;
} PROV_PARAMS, PPROV_PARAMS;
typedef struct PROVIDERS_T {
PROV_PARAMS params;
} PROVIDERS, PPROVIDERS;
Finally, we declare an array of PROVIDERS
and initialize it with the variants of XP/non-XP, and open existing versus create new as follows. We initialize the elements in the order in which we want to acquire the provider. For example, we prefer to open an existing container rather than creating a new container.
const PROVIDERS AesProviders[] =
{
{ MS_ENH_RSA_AES_PROV, PROV_RSA_AES, 0 },
{ MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_NEWKEYSET },
{ MS_ENH_RSA_AES_PROV_XP, PROV_RSA_AES, 0 },
{ MS_ENH_RSA_AES_PROV_XP, PROV_RSA_AES, CRYPT_NEWKEYSET },
};
To obtain a handle to a provider, we perform the following. The _countof
operator is provided by the compiler (available in Visual Studio 2005 and above), and properly handles pointers.
for( int i = 0; i < _countof(AesProviders); i++ )
{
if( CryptAcquireContext(
&hProvider, lpszContainer,
AesProviders[i].params.lpwsz,
AesProviders[i].params.dwType,
AesProviders[i].params.dwFlags ) )
{
nIndex = i;
break;
}
}
We must retain the index which acquired the provider in case the key set is to be deleted. While the strategy to open any suitable provider during construction is acceptable, we cannot delete containers in the destructor using the same method. We have to know exactly what provider/type was used so we can delete the exact key set.
Keying
bool SetKey( const byte* key, int ksize = KEYSIZE_128 )
bool SetIV( const byte* iv, int vsize=BLOCKSIZE )
bool SetKeyWithIV( const byte* key, int ksize, const byte* iv, int vsize=BLOCKSIZE )
We have three key sizes available. AES-128 corresponds to the enumerated value KEYSIZE_128
. The remaining sizes, KEYSIZE_192
and KEYSIZE_256
, represent AES-192 and AES-256. The initialization vector is 128 bits (BLOCKSIZE
).
We can call either the pair SetKey
/SetIV
or we can call SetKeyWithIV
. If calling the former, the key must be set before the IV. Some DLLs allow us to get into a configuration where the CBC mode is selected, a key is set, but no IV is set; and the encryption operation will not fail as expected. This is due to the lack of state validation by the CSP. We correct this behavior in our WinAES
object.
At anytime, we can resynchronize the object by calling SetIV
. We do not have to import a new key, or re-import an existing key to load a new IV.
CryptImportKey
CryptImportKey
expects keys to be in a certain format. The format that the function expects is a BLOBHEADER
, followed by the key size (in bytes), followed by the actual key material. Depending on the key size, the number of bytes imported using CryptImportKey
will vary due to the number of bytes of the actual key. We could do this in one of two ways: the easy way (stack allocations) or the hard way (runtime allocations). Obviously, we are going to use the easy way. The runtime allocation method is left as an exercise to the reader.
To use a stack allocation, we need an auxiliary structure named AesKey
. To accommodate the largest key possible, we will define the structure in terms of AES-256 and adjust its size at runtime before calling CryptImportKey
. We can always put fewer bytes of key material into the structure. So, the declaration is as follows.
typedef struct _AesKey
{
BLOBHEADER Header;
DWORD dwKeyLength;
BYTE cbKey[KEYSIZE_256];
_AesKey() {
ZeroMemory( this, sizeof(_AesKey) );
Header.bType = PLAINTEXTKEYBLOB;
Header.bVersion = CUR_BLOB_VERSION;
Header.reserved = 0;
}
~_AesKey() {
SecureZeroMemory( this, sizeof(_AesKey) );
}
}
The only difference between a struct
and a class
in C++ is the default visibility of members - a struct
is public
while a class
is private
. So, we treat the struct
as a class
, and provide a constructor to initialize the BLOBHEADER
, and a destructor which scrubs any key material from memory. We also turn optimizations off for the destructor so that the call to SecureZeroMemory
is not marked as dead code and subsequently removed. It does not matter that we also zeroize the BLOBHEADER
(though the performance oriented purist may complain).
#pragma optimize( "", off )
~_AesKey() {
SecureZeroMemory( this, sizeof(_AesKey) );
}
#pragma optimize( "", on )
Also note that when the MSDN sample code calls CryptGenKey
or CryptDeriveKey
, and then later exports the key, it is exporting a _AesKey
(in the particular case using AES). The key material is preceded by an appropriate BLOBHEADER
and the length. The BLOBHEADER
and dwKeyLength
is what Raphael Amorim is stepping over in his CodeProject article, Obtain the plain text session key using CryptoAPI. For completeness, the BLOBHEADER
from wincrypt.h is shown below:
typedef struct _PUBLICKEYSTRUC {
BYTE bType;
BYTE bVersion;
WORD reserved;
ALG_ID aiKeyAlg;
} BLOBHEADER, PUBLICKEYSTRUC;
With the AesKey
structure explained, we can now examine SetKey
. The signature for the function is SetKey( const byte* key, int ksize )
. Below, we place the structure on the stack, and immediately populate the remaining structure fields based on the parameter ksize
. ksize
will be either 16, 24, or 32 depending on whether the caller is using AES-128, AES-192, or AES-256, respectively.
AesKey aeskey;
switch( ksize )
{
case KEYSIZE_128:
aeskey.Header.aiKeyAlg = CALG_AES_128;
aeskey.dwKeyLength = KEYSIZE_128;
break;
case KEYSIZE_192:
aeskey.Header.aiKeyAlg = CALG_AES_192;
aeskey.dwKeyLength = KEYSIZE_192;
break;
case KEYSIZE_256:
aeskey.Header.aiKeyAlg = CALG_AES_256;
aeskey.dwKeyLength = KEYSIZE_256;
break;
default:
...
}
We copy the key from the caller into the structure using a safe memory copy: memcpy_s(aeskey.cbKey, aeskey.dwKeyLength, key, ksize)
. Recall that the key material being copied will be scrubbed by the destructor when the function exits.
Then, we adjust the size of the structure. When using a 128 bit key, structsize
is 0x1C, and when using a 256 bit key, structsize
is 0x2C. Exactly in between is the 192 bit key.
const unsigned structsize = sizeof(aeskey) - KEYSIZE_256 + ksize;
if(!CryptImportKey(hProvider, (CONST BYTE*)&aeskey, structsize, NULL, 0, &hAesKey ) )
{
...
}
CryptSetKeyParam
After we import the key, we make a call to CryptSetKeyParam
to make sure the cipher is operated in CBC mode. Though redundant (it is supposed to be the default mode), we will not risk rogue or undocumented behavior from the DLLs.
DWORD dwMode = CRYPT_MODE_CBC;
if(!CryptSetKeyParam( hAesKey, KP_MODE, (BYTE*)&dwMode, 0 ))
{
...
}
Encryption/Decryption
We are almost to the point of encryption and decryption. Before we can encrypt, we need to know the size of the buffer required for the cipher text. When operating a cipher in CBC mode, the plain text must be padded to the cipher's block size in an unambiguous manner so that the padding can later be removed. PKCS #5 is one such scheme. We do not apply or remove the padding, but we need to know how CAPI does it so we can provide the proper size buffers.
PKCS #5
PKCS #5 works as follows: if the required padding is 1 byte, 0x01 is appended to the plain text. If 2 bytes is required, 0x02, 0x02 is appended to the plain text. This leaves one case: what to do when 0 bytes are required. In this case, 0x16 is appended 16 times. Though a bit counter-intuitive, this allows for unambiguous removal of the padding.
Armed with the knowledge of PKCS #5, WinAES
offers two function for determining plain text and cipher text sizes: MaxCipherTextSize
and MaxPlainTextSize
. Since we cannot tell how much padding will be removed from the cipher text until it is decrypted, MaxPlainTextSize
will always return the cipher text size. The buffer will always be a bit too large, but never larger than BLOCKSIZE
since BLOCKSIZE
is the most padding which will have to be removed.
Encrypt
There are two overloads for encryption. One allows for encrypting a buffer in place, the other uses two distinct buffers. If using the second overload (two buffers), the buffers must not overlap.
bool Encrypt(byte* buffer, size_t bsize, size_t psize, size_t& csize)
bool Encrypt(const byte* plaintext, size_t psize, byte* ciphertext, size_t& csize)
When using a common buffer, bsize
is the size of the buffer, and psize
is the size of the plain text. On successful return, csize
is the size of the cipher text. The case of distinct buffers is easier since it only uses psize
and csize
. If the object is not configured to throw, we receive a true
/false
back. Otherwise, we must be prepared to catch a WinAESException
.
Decrypt
There are two overloads for decryption. The first allows for decrypting a buffer in place, the second uses two distinct buffers. If using the second overload (two buffers), the buffers must not overlap.
bool Decrypt(byte* buffer, size_t bsize, size_t csize, size_t& psize)
bool Decrypt(const byte* ciphertext, size_t csize, byte* plaintext, size_t& psize)
When using a common buffer, bsize
is the size of the buffer, and psize
is the size of the plain text. On successful return, psize
is the size of the plain text.
Sample Program
Now that WinAES
has been introduced, we can look at the class in action. The class does provide access to the CSP's CryptGenRandom
function, so we use it below to produce a key and IV. The sample program below (which includes WinAES
) is available for download. In an attempt to reduce the displayed code, exception handling and failures have been omitted.
WinAES aes;
byte key[ WinAES::KEYSIZE_128 ];
byte iv[ WinAES::BLOCKSIZE ];
aes.GenerateRandom( key, sizeof(key) );
aes.GenerateRandom( iv, sizeof(iv) );
aes.SetKeyWithIv( key, sizeof(key), iv, sizeof(iv) );
char plaintext[] = "Microsoft AES Cryptographic Service Provider test";
byte *ciphertext = NULL, *recovered = NULL;
size_t psize=0, csize=0, rsize=0;
psize = strlen( plaintext ) + 1;
if( aes.MaxCipherTextSize( psize, csize ) ) {
ciphertext = new byte[ csize ];
}
if( !aes.Encrypt( (byte*)plaintext, psize, ciphertext, csize ) ) {
cerr << "Failed to encrypt plain text" << endl;
}
if( aes.MaxPlainTextSize( csize, rsize ) ) {
recovered = new byte[ rsize ];
}
if( !aes.Decrypt( ciphertext, csize, recovered, rsize ) ) {
cerr << "Failed to decrypt cipher text" << endl;
}
...
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.