5.25 Using Symmetric Encryption with Microsoft's CryptoAPI

5.25.1 Problem

You are developing an application that will run on Windows and make use of symmetric encryption. You want to use Microsoft's CryptoAPI.

5.25.2 Solution

Microsoft's CryptoAPI is available on most versions of Windows that are widely deployed, so it is a reasonable solution for many uses of symmetric encryption. CryptoAPI contains a small, yet nearly complete, set of functions for creating and manipulating symmetric encryption keys (which the Microsoft documentation usually refers to as session keys), exchanging keys, and encrypting and decrypting data. While the information in the following Section 5.25.3 will not provide you with all the finer details of using CryptoAPI, it will give you enough background to get started using the API successfully.

5.25.3 Discussion

CryptoAPI is designed as a high-level interface to various cryptographic constructs, including hashes, MACs, public key encryption, and symmetric encryption. Its support for public key cryptography makes up the majority of the API, but there is also a small subset of functions for symmetric encryption.

Before you can do anything with CryptoAPI, you first need to acquire a provider context. CryptoAPI provides a generic API that wraps around Cryptographic Service Providers (CSPs), which are responsible for doing all the real work. Microsoft provides several different CSPs that provide implementations of various algorithms. For symmetric cryptography, two CSPs are widely available and of interest: Microsoft Base Cryptographic Service Provider and Microsoft Enhanced Cryptographic Service Provider. A third, Microsoft AES Cryptographic Service Provider, is available only in the .NET framework. The Base CSP provides RC2, RC4, and DES implementations. The Enhanced CSP adds implementations for DES, two-key Triple-DES, and three-key Triple-DES. The AES CSP adds implementations for AES with 128-bit, 192-bit, and 256-bit key lengths.

For our purposes, we'll concentrate only on the enhanced CSP. Acquiring a provider context is done with the following code. We use the CRYPT_VERIFYCONTEXT flag here because we will not be using private keys with the context. It doesn't necessarily hurt to omit the flag (which we will do in Recipe 5.26 and Recipe 5.27, for example), but if you don't need public key access with the context, you should use the flag. Some CSPs may require user input when CryptAcquireContext( ) is called without CRYPT_VERIFYCONTEXT.

#include <windows.h>
#include <wincrypt.h>
   
HCRYPTPROV SpcGetCryptContext(void) {
  HCRYPTPROV hProvider;
   
  if (!CryptAcquireContext(&hProvider, 0, MS_ENHANCED_PROV, PROV_RSA_FULL,
                           CRYPT_VERIFYCONTEXT)) return 0;
  return hProvider;
}

Once a provider context has been successfully acquired, you need a key. The API provides three ways to obtain a key object, which is stored by CryptoAPI as an opaque object to which you'll have only a handle:

CryptGenKey( )

Generates a random key.

CryptDeriveKey( )

Derives a key from a password or passphrase.

CryptImportKey( )

Creates a key object from key data in a buffer.

All three functions return a new key object that keeps the key data hidden and has associated with it a symmetric encryption algorithm and a set of flags that control the behavior of the key. The key data can be obtained from the key object using CryptExportKey( ) if the key object allows it. The CryptExportKey( ) and CryptImportKey( ) functions provide the means for exchanging keys.

The CryptExportKey( ) function will only allow you to export a symmetric encryption key encrypted with another key. For maximum portability across all versions of Windows, a public key should be used. However, Windows 2000 introduced the ability to encrypt the symmetric encryption key with another symmetric encryption key. Similarly, CryptImportKey( ) can only import symmetric encryption keys that are encrypted.

If you need the raw key data, you must first export the key in encrypted form, then decrypt from it (see Recipe 5.27). While this may seem like a lot of extra work, the reason is that CryptoAPI was designed with the goal of making it very difficult (if not impossible) to unintentionally disclose sensitive information.

Generating a new key with CryptGenKey( ) that can be exported is very simple, as illustrated in the following code. If you don't want the new key to be exportable, simply remove the CRYPT_EXPORTABLE flag.

HCRYPTKEY SpcGetRandomKey(HCRYPTPROV hProvider, ALG_ID Algid, DWORD dwSize) {
  DWORD     dwFlags;
  HCRYPTKEY hKey;
   
  dwFlags = ((dwSize << 16) & 0xFFFF0000) | CRYPT_EXPORTABLE;
  if (!CryptGenKey(hProvider, Algid, dwFlags, &hKey)) return 0;
  return hKey;
}

Deriving a key with CryptDeriveKey( ) is a little more complex. It requires a hash object to be created and passed into it in addition to the same arguments required by CryptGenKey( ). Note that once the hash object has been used to derive a key, additional data cannot be added to it, and it should be immediately destroyed.

HCRYPTKEY SpcGetDerivedKey(HCRYPTPROV hProvider, ALG_ID Algid, LPTSTR password) {
  BOOL       bResult;
  DWORD      cbData;
  HCRYPTKEY  hKey;
  HCRYPTHASH hHash;
   
  if (!CryptCreateHash(hProvider, CALG_SHA1, 0, 0, &hHash)) return 0;
  cbData = lstrlen(password) * sizeof(TCHAR);
  if (!CryptHashData(hHash, (BYTE *)password, cbData, 0)) {
    CryptDestroyHash(hHash);
    return 0;
  }
  bResult = CryptDeriveKey(hProvider, Algid, hHash, CRYPT_EXPORTABLE, &hKey);
  CryptDestroyHash(hHash);
  return (bResult ? hKey : 0);
}

Importing a key with CryptImportKey( ) is, in most cases, just as easy as generating a new random key. Most often, you'll be importing data obtained directly from CryptExportKey( ), so you'll already have an encrypted key in the form of a SIMPLEBLOB, as required by CryptImportKey( ). If you need to import raw key data, things get a whole lot trickier?see Recipe 5.26 for details.

HCRYPTKEY SpcImportKey(HCRYPTPROV hProvider, BYTE *pbData, DWORD dwDataLen,
                       HCRYPTKEY hPublicKey) {
  HCRYPTKEY  hKey;
   
  if (!CryptImportKey(hProvider, pbData, dwDataLen, hPublicKey, CRYPT_EXPORTABLE,
                      &hKey)) return 0;
  return hKey;
}

When a key object is created, the cipher to use is tied to that key, and it must be specified as an argument to either CryptGenKey( ) or CryptDeriveKey( ). It is not required as an argument by CryptImportKey( ) because the cipher information is stored as part of the SIMPLEBLOB structure that is required. Table 5-8 lists the symmetric ciphers that are available using one of the three Microsoft CSPs.

Table 5-8. Symmetric ciphers supported by Microsoft Cryptographic Service Providers

Cipher

Cryptographic Service Provider

ALG_ID constant

Key length

Block size

RC2

Base, Enhanced, AES

CALG_RC2

40 bits

64 bits

RC4

Base

CALG_RC4

40 bits

n/a

RC4

Enhanced, AES

CALG_RC4

128 bits

n/a

DES

Enhanced, AES

CALG_DES

56 bits

64 bits

2-key Triple-DES

Enhanced, AES

CALG_3DES_112

112 bits (effective)

64 bits

3-key Triple-DES

Enhanced, AES

CALG_3DES

168 bits (effective)

64 bits

AES

AES

CALG_AES_128

128 bits

128 bits

AES

AES

CALG_AES_192

192 bits

128 bits

AES

AES

CALG_AES_256

256 bits

128 bits

The default cipher mode to be used depends on the underlying CSP and the algorithm that's being used, but it's generally CBC mode. The Microsoft Base and Enhanced CSPs provide support for CBC, CFB, ECB, and OFB modes (see Recipe 5.4 for a discussion of cipher modes). The mode can be set using the CryptSetKeyParam( ) function:

BOOL SpcSetKeyMode(HCRYPTKEY hKey, DWORD dwMode) {
  return CryptSetKeyParam(hKey, KP_MODE, (BYTE *)&dwMode, 0);
}
   
#define SpcSetMode_CBC(hKey) SpcSetKeyMode((hKey), CRYPT_MODE_CBC)
#define SpcSetMode_CFB(hKey) SpcSetKeyMode((hKey), CRYPT_MODE_CFB)
#define SpcSetMode_ECB(hKey) SpcSetKeyMode((hKey), CRYPT_MODE_ECB)
#define SpcSetMode_OFB(hKey) SpcSetKeyMode((hKey), CRYPT_MODE_OFB)

In addition, the initialization vector for block ciphers will be set to zero, which is almost certainly not what you want. The function presented below, SpcSetIV( ), will allow you to set the IV for a key explicitly or will generate a random one for you. The IV should always be the same size as the block size for the cipher in use.

BOOL SpcSetIV(HCRYPTPROV hProvider, HCRYPTKEY hKey, BYTE *pbIV) {
  BOOL  bResult;
  BYTE  *pbTemp;
  DWORD dwBlockLen, dwDataLen;
   
  if (!pbIV) {
    dwDataLen = sizeof(dwBlockLen);
    if (!CryptGetKeyParam(hKey, KP_BLOCKLEN, (BYTE *)&dwBlockLen, &dwDataLen, 0))
      return FALSE;
    dwBlockLen /= 8;
    if (!(pbTemp = (BYTE *)LocalAlloc(LMEM_FIXED, dwBlockLen))) return FALSE;
    bResult = CryptGenRandom(hProvider, dwBlockLen, pbTemp);
    if (bResult)
      bResult = CryptSetKeyParam(hKey, KP_IV, pbTemp, 0);
    LocalFree(pbTemp);
    return bResult;
  }
  return CryptSetKeyParam(hKey, KP_IV, pbIV, 0);
}

Once you have a key object, it can be used for encrypting and decrypting data. Access to the low-level algorithm implementation is not permitted through CryptoAPI. Instead, a high-level OpenSSL EVP-like interface is provided (see Recipe 5.17 and Recipe 5.22 for details on OpenSSL's EVP API), though it's somewhat simpler. Both encryption and decryption can be done incrementally, but there is only a single function for each.

The CryptEncrypt( ) function is used to encrypt data all at once or incrementally. As a convenience, the function can also pass the plaintext to be encrypted to a hash object to compute the hash as data is passed through for encryption. CryptEncrypt( ) can be somewhat tricky to use because it places the resulting ciphertext into the same buffer as the plaintext. If you're using a stream cipher, this is no problem because the ciphertext is usually the same size as the plaintext, but if you're using a block cipher, the ciphertext can be up to a whole block longer than the plaintext. The following convenience function handles the buffering issues transparently for you. It requires the spc_memcpy( ) function from Recipe 13.2.

BYTE *SpcEncrypt(HCRYPTKEY hKey, BOOL bFinal, BYTE *pbData, DWORD *cbData) {
  BYTE   *pbResult;
  DWORD  dwBlockLen, dwDataLen;
  ALG_ID Algid;
   
  dwDataLen = sizeof(ALG_ID);
  if (!CryptGetKeyParam(hKey, KP_ALGID, (BYTE *)&Algid, &dwDataLen, 0)) return 0;
  if (GET_ALG_TYPE(Algid) != ALG_TYPE_STREAM) {
    dwDataLen = sizeof(DWORD);
    if (!CryptGetKeyParam(hKey, KP_BLOCKLEN, (BYTE *)&dwBlockLen, &dwDataLen, 0))
      return 0;
    dwDataLen = ((*cbData + (dwBlockLen * 2) - 1) / dwBlockLen) * dwBlockLen;
    if (!(pbResult = (BYTE *)LocalAlloc(LMEM_FIXED, dwDataLen))) return 0;
    CopyMemory(pbResult, pbData, *cbData);
    if (!CryptEncrypt(hKey, 0, bFinal, 0, pbResult, &dwDataLen, *cbData)) {
      LocalFree(pbResult);
      return 0;
    }
    *cbData = dwDataLen;
    return pbResult;
  }
   
  if (!(pbResult = (BYTE *)LocalAlloc(LMEM_FIXED, *cbData))) return 0;
  CopyMemory(pbResult, pbData, *cbData);
  if (!CryptEncrypt(hKey, 0, bFinal, 0, pbResult, cbData, *cbData)) {
    LocalFree(pbResult);
    return 0;
  }
  return pbResult;
}

The return from SpcEncrypt( ) will be a buffer allocated with LocalAlloc( ) that contains the ciphertext version of the plaintext that's passed as an argument into the function as pbData. If the function fails for some reason, the return from the function will be NULL, and a call to GetLastError( ) will return the error code. This function has the following arguments:

hKey

Key to use for performing the encryption.

bFinal

Boolean value that should be passed as FALSE for incremental encryption except for the last piece of plaintext to be encrypted. To encrypt all at once, pass TRUE for bFinal in the single call to SpcEncrypt( ). When CryptEncrypt( ) gets the final plaintext to encrypt, it performs any cleanup that is needed to reset the key object back to a state where a new encryption or decryption operation can be performed with it.

pbData

Plaintext.

cbData

Pointer to a DWORD type that should hold the length of the plaintext pbData buffer. If the function returns successfully, it will be modified to hold the number of bytes returned in the ciphertext buffer.

Decryption works similarly to encryption. The function CryptDecrypt( ) performs decryption either all at once or incrementally, and it also supports the convenience function of passing plaintext data to a hash object to compute the hash of the plaintext as it is decrypted. The primary difference between encryption and decryption is that when decrypting, the plaintext will never be any longer than the ciphertext, so the handling of data buffers is less complicated. The following function, SpcDecrypt( ), mirrors the SpcEncrypt( ) function presented previously.

BYTE *SpcDecrypt(HCRYPTKEY hKey, BOOL bFinal, BYTE *pbData, DWORD *cbData) {
  BYTE   *pbResult;
  DWORD  dwBlockLen, dwDataLen;
  ALG_ID Algid;
   
  dwDataLen = sizeof(ALG_ID);
  if (!CryptGetKeyParam(hKey, KP_ALGID, (BYTE *)&Algid, &dwDataLen, 0)) return 0;
  if (GET_ALG_TYPE(Algid) != ALG_TYPE_STREAM) {
    dwDataLen = sizeof(DWORD);
    if (!CryptGetKeyParam(hKey, KP_BLOCKLEN, (BYTE *)&dwBlockLen, &dwDataLen, 0))
      return 0;
    dwDataLen = ((*cbData + dwBlockLen - 1) / dwBlockLen) * dwBlockLen;
    if (!(pbResult = (BYTE *)LocalAlloc(LMEM_FIXED, dwDataLen))) return 0;
  } else {
    if (!(pbResult = (BYTE *)LocalAlloc(LMEM_FIXED, *cbData))) return 0;
  }
  CopyMemory(pbResult, pbData, *cbData);
  if (!CryptDecrypt(hKey, 0, bFinal, 0, pbResult, cbData)) {
    LocalFree(pbResult);
    return 0;
  }
  return pbResult;
}

Finally, when you're finished using a key object, be sure to destroy the object by calling CryptDestroyKey( ) and passing the handle to the object to be destroyed. Likewise, when you're done with a provider context, you must release it by calling CryptReleaseContext( ).

5.25.4 See Also

Recipe 5.4, Recipe 5.17, Recipe 5.22, Recipe 5.26, Recipe 5.27, Recipe 13.2