You are developing an application that will run on Windows and make use of symmetric encryption. You want to use Microsoft's CryptoAPI.
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.
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:
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 |
|
40 bits |
64 bits |
RC4 |
Base |
|
40 bits |
n/a |
RC4 |
Enhanced, AES |
|
128 bits |
n/a |
DES |
Enhanced, AES |
|
56 bits |
64 bits |
2-key Triple-DES |
Enhanced, AES |
|
112 bits (effective) |
64 bits |
3-key Triple-DES |
Enhanced, AES |
|
168 bits (effective) |
64 bits |
AES |
AES |
|
128 bits |
128 bits |
AES |
AES |
|
192 bits |
128 bits |
AES |
AES |
|
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(
)
.