Share via


The Cryptography API, or How to Keep a Secret

 

Robert Coleridge
Microsoft Developer Network Technology Group

August 19, 1996

Abstract

This article describes the Microsoft® Cryptography application programming interface (API) that is available with the new Windows NT® version 4.0 release and upcoming versions of Windows® 95. This article examines what is required to set up and use this new API. In order to compile the sample application you will need Microsoft Visual C++® version 4.2 or later and Windows NT 4.0 or later.

Note    Portions of the Cryptography API fall under U.S. export restrictions.

Download 5003.exe.

Introduction

The Cryptography API has a number of significant uses within the Enterprise Computing Model. Computing on an Enterprise scale implies a more global framework for interaction between people, such as international commodity trading, interstate inventory management, and so on. Within that framework it is often necessary to transmit sensitive information over non-secure channels—for example, faxing contracts, e-mailing buy or sell orders, and so on. By using the Cryptography API, you can guarantee the security of such information.

This article uses the CRYPTOAPI sample application to demonstrate how to decrypt or encrypt data, sign and verify files, and add and remove users.

An Overview of the Cryptography API

Cryptography Service Providers (CSPs)

[Editor's note: Portions of this article are quoted directly from the CryptoAPI documentation in the Platform SDK. Those sections of text are indented (all text is normally flush left) but not preceded by a number, a bullet, or the word "Note:", so that you can easily identify them.]

The Cryptography API contains functions that allow applications to encrypt or digitally sign data in a flexible manner, while providing protection for the user's sensitive private key data. All cryptographic operations are performed by independent modules known as cryptographic service providers (CSPs). One CSP, the Microsoft RSA Base Provider, is included with the operating system.

Each CSP provides a different implementation of the Cryptography API layer. Some provide stronger cryptographic algorithms, while others contain hardware components such as smartcards [plastic cards containing microchips that hold security data about the user]. In addition, some CSPs may occasionally communicate with users directly, such as when digital signatures are performed using the user's signature private key.

Applications should not take advantage of attributes particular to a specific CSP. For example, the Microsoft RSA Base Provider currently uses 40-bit session keys and 512-bit public keys. (See "Symmetric Versus Public-Key Encryption," MSDN Library, Platform, SDK, and DDK Documentation) When applications manipulate these, they should be careful not to make assumptions about the amount of memory needed to store them. Otherwise, the application is likely to fail when the user loads a different CSP onto the system. You should take care to write applications that are as well-behaved and flexible as possible.

Key Databases

Each CSP has a key database in which it stores its persistent cryptographic keys. Each key database contains one or more key containers, each of which contains all the key pairs belonging to a specific user (or Cryptography API client). Each key container is given a unique name, which applications provide to the CryptAcquireContext function when acquiring a handle to the key container. Figure 1 is an illustration of the contents of a key database:

ms867086.cryptapi_1(en-us,MSDN.10).gif

Figure 1. Contents of a key database

The CSP stores each key container from session to session, including all the public/private key pairs it contains. However, session keys are not preserved from session to session.

Although it is possible to find these keys on a computer, they are stored in an encrypted and secure format.

Generally, a default key container is created for each user. This key container takes the user's 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.

Keys

Session Keys

Session keys are used when encrypting and decrypting data. They are created by applications using either the CryptGenKey or the CryptDeriveKey function. These keys are kept inside the CSP for safekeeping.

Unlike the key pairs, session keys are volatile. Applications can save these keys for later use or transmission to other users by exporting them from the CSP into application space in the form of an encrypted key binary large object or key blob using the CryptExportKey function.

Public or Private Key Pairs

Each user generally has two public or private key pairs. One key pair is used to encrypt session keys and the other to create digital signatures. These are known as the key exchange key pair and the signature key pair, respectively.

Note that although key containers created by most CSPs will contain two key pairs, this is not required. Some CSPs do not store any key pairs, while others store additional ones.

Encryption

In using data encryption, a plain-text message can be encoded so it appears as completely random binary data that is very difficult (if not impossible) to transform back to the original message without a secret key. In this article, the following definitions apply:

  • Message is used to refer to any piece of data. A message can consist of ASCII text, a database file, or any data you want to store or transmit securely.
  • Plain text is used to refer to data that has not been encrypted.
  • Cipher text refers to data that has been encrypted.

Once a message has been encrypted, it can be stored on nonsecure media or transmitted on a nonsecure network and still remain secret. Later, the message can be decrypted into its original form. This process is shown in Figure 2.

ms867086.cryptapi_2(en-us,MSDN.10).gif

Figure 2. Encrypting and decrypting a message

When a message is encrypted, an encryption key is used. This is analogous to the physical key that is used to lock a padlock. To decrypt the message, the corresponding decryption key must be used. It is very important to properly restrict access to the decryption key, because anyone who possesses it will be able to decrypt all messages that were encrypted with the matching encryption key.

This may come as a surprise, but data encryption/decryption is pretty straightforward. The really difficult part is keeping the keys safe and transmitting them securely to other users. This topic is beyond the scope of this article but I would recommend that the reader read the section titled "Exchanging Cryptographic Keys" in the Win32® Cryptography API documentation (MSDN Library, Platform, SDK, and DDK Documentation).

There are two main classes of encryption algorithms: symmetric algorithms and public-key algorithms (also known as asymmetric algorithms). Systems that use symmetric algorithms are sometimes referred to as conventional.

Algorithms

Symmetric algorithms are the most common type of encryption algorithm. They are known as "symmetric" because the same key is used for both encryption and decryption. Unlike the keys used with public-key algorithms, symmetric keys are frequently changed. For this reason, they are referred to here as session keys. Compared to public-key algorithms, symmetric algorithms are very fast and thus are preferred when encrypting large amounts of data. Some of the more common symmetric algorithms are RC2, RC4, and the Data Encryption Standard (DES).

Public-key (asymmetric) algorithms use a pair of different keys: a public key and a private key. The private key is kept private to the owner of the key pair, and the public key can be distributed to anyone who requests it. If one key is used to encrypt a message, the other key is required to decrypt the message. Public-key algorithms are very slow, on the order of a thousand times slower than symmetric algorithms. Consequently, they are normally used only to encrypt session keys. They are also used to digitally sign messages, as discussed in the next section. One of the most common public-key algorithms is the RSA Public-Key Cipher.

File Signing

Digital signatures can be used when you have a message that you plan to distribute in plain-text form, and you want the recipients to be able to verify that the message comes from you and that it hasn't been tampered with since it left your hands. Signing a message does not alter the message, it simply generates a digital signature string you can bundle with the message or transmit separately.

Digital signatures are generated using public-key signature algorithms. A private key is used to generate the signature, and the corresponding public key is used to validate the signature. This process is shown in Figure 3.

ms867086.cryptapi_3(en-us,MSDN.10).gif

Figure 3. Validating a signature

Some Cryptography API Functions

[Editor's note: Indented portions of the following text are quoted from MSDN Library, Platform, SDK, and DDK Documentation.]

Initiating the CSP: CryptAcquireContext, CryptReleaseContext

The CryptAcquireContext function is used to obtain a handle to a particular key container within a particular CSP. This returned handle can then be used to make calls to the selected CSP.

The CryptReleaseContext function is used to release the handle returned from a call to CryptAcquireContext. The CryptReleaseContext function does not delete any Cryptography API objects, but merely releases the handle to an object.

The CryptAcquireContext function performs two operations. It first attempts to find a CSP with the characteristics described by various parameters. If the CSP is found, the function attempts to find a key container within the CSP that matches the specified container name. This function can also be used to create and destroy key containers, depending on the value of the parameters.

To obtain a handle to the default key container of the default CSP the code would look like this:

#include <wincrypt.h>      // CryptoAPI definitions
/*
For non-C/C++ users the constants used here are:
#define MS_DEF_PROV       "Microsoft Base Cryptographic Provider v1.0"
#define PROV_RSA_FULL           1
*/

BOOL bResult;
HCRYPTPROC hProv;

// Attempt to acquire a handle to the default key container.
bResult = CryptAcquireContext(
            &hProv,            // Variable to hold returned handle.
            NULL,              // Use default key container.
            MS_DEF_PROV,       // Use default CSP.
            PROV_RSA_FULL,     // Type of provider to acquire.
            0);                // No special action.
.
.
.
//Do some work.
.
.
.
// Release handle to container.
CryptReleaseContext(hProv);

If the call to CryptAcquireContext is successful, the return code will be non-zero and the variable hProv will be a handle to the requested key container.

In order to add or create a key container for the default CSP we would write code like the following:

#include <wincrypt.h>      // CryptoAPI definitions
/*
For non C/C++ users the constants used here are:
#define MS_DEF_PROV       "Microsoft Base Cryptographic Provider v1.0"
#define PROV_RSA_FULL           1
#define CRYPT_NEWKEYSET         0x8
*/

BOOL bResult;
HCRYPTPROC hProv;

// Attempt to add a new key container.
BResult = CryptAcquireContext(
            &hProv,              // Variable to hold returned handle.
            NULL,                // Use default key container.
            MS_DEF_PROV,         // Use default CSP.
            PROV_RSA_FULL,       // Type of provider to acquire.
            CRYPT_NEWKEYSET);    // Create new key container.
.
.
.
//Do some work.
.
.
.
// Release handle to container.
CryptReleaseContext(hProv);

If the call to CryptAcquireContext is successful, the return code will be non-zero and the variable hProv will be a handle to the new key container.

In order to delete an existing key container from the default CSP, we would write code like the following:

#include <wincrypt.h>      // CryptoAPI definitions
/*
For non C/C++ users the constants used here are:
#define MS_DEF_PROV       "Microsoft Base Cryptographic Provider v1.0"
#define PROV_RSA_FULL           1
#define CRYPT_DELETEKEYSET         0x10
*/
BOOL bResult;
HCRYPTPROC hProv;

// Attempt to delete key container.
BResult = CryptAcquireContext(
            &hProv,                // Variable to hold returned handle.
            NULL,                  // Use default key container.
            MS_DEF_PROV,           // Use default CSP.
            PROV_RSA_FULL,         // Type of provider to acquire.
            CRYPT_DELETEKEYSET);   // Delete existing key container.

If the call to CryptAcquireContext is successful, the return code will be non-zero, the key container pointed to by hProv will have been deleted, and the key container will no longer be valid.

Hashing Data: CryptCreateHash, CryptHashData, CryptGetHashParam, and CryptDestroyHash

When I say "hashing" or "hash," I am referring to the method or algorithm used to derive a numeric value from a piece of data. This could be something as simple as adding up all of the one bits in the data, or as complicated as doing a Fourier transformation of the data.

The four functions listed in the heading above are used to create or manipulate hash value from supplied data, and are usually used together:

  • The CryptCreateHash function is used to initiate the hashing of data. It returns a handle to a CSP hash object, which can be used in subsequent calls to CryptHashData in order to hash the data.
  • The next step is to use the CryptGetHashParam function to retrieve the hash value.
  • The CryptDestroyHash function is used to release the handle returned from CryptCreateHash. CryptDestroyHash does not delete any Cryptography API objects, but merely releases the handle to a hash object.

The CryptHashData function is used to compute the cryptographic hash of some supplied data. This function can be called multiple times to compute the hash on large data or different pieces of data. As an example, we will hash the data that is contained in a buffer pointed to by pBuffer and that is dwBufferLen bytes long. I have chosen the CALG_MD5 hashing algorithm for the purpose of this example only. There are many other algorithms available and fully explained in the Cryptography API SDK documentation. This example assumes only one piece of data to hash. Once the hash value has been retrieved via CryptGetHashParam, no more data can be hashed with this instance of the hash object.

#include <wincrypt.h>      // CryptoAPI definitions
/*
For non C/C++ users the constants used here are:
#define ALG_CLASS_HASH                  (4 << 13)
#define ALG_TYPE_ANY                    (0)
#define ALG_SID_MD5                     3
#define CALG_MD5        (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5)
#define HP_HASHVAL              0x0002  // Hash value
#define HP_HASHSIZE             0x0004  // Hash value size
*/
BOOL bResult;
HCRYPTHASH hHash;
DWORD dwBufferSize;
DWORD dwValue;
PBYTE pBuffer;

// Obtain handle to hash object.
bResult = CryptCreateHash(
            hProv,               // Handle to CSP obtained earlier
            CALG_MD5,            // Hashing algorithm
            0,                   // Non-keyed hash
            0,                   // Should be zero
            &hHash);             // Variable to hold hash object handle 

// Hash data.
bResult = CryptHashData(
            hHash,               // Handle to hash object
            pBuffer,             // Pointer to data buffer
            dwBufferlen,         // Length of data
            0);                  // No special flags

// Get size of hash value.
dwBufferSize = sizeof(DWORD);
bResult = CryptGetHashParam(
            hHash,               // Handle to hash object
            HP_HASHSIZE,         // Get hash value
            &dwValue,            // Buffer to hold hash value length
            &dwBufferSize,       // Length of data buffer
            0);                  // Must be zero

// Create buffer to hold hash value.
pBuffer = new char [dwBufferSize];

// Get hash value.
bResult = CryptGetHashParam(
            hHash,              // Handle to hash object
            HP_HASHVAL,         // Get hash value
            pBuffer,            // Buffer to hold hash value
            &dwBufferSize,      // Length of data
            0);                 // Must be zero

// Release hash object.
CryptDestroyHash(hHash);

The above example generated a hash value for the data pointed to by pBuffer. If there was more data to hash, calling CryptHashData with that data would have hashed the new data with the old value. Be warned—calling CryptGetHashParam with the HP_HASHVALUE parameter prevents any further hashing with that particular object.

Generating Keys: CryptDeriveKey, CryptGenKey, CryptDestroyKey

These three functions are the ones used to generate handles to keys:

  • The CryptDeriveKey function is used to generate a key from a specified password.
  • The CryptGenKey function is used to generate a key from random generated data.
  • The CryptDestroyKey function is used to release the handle to the key object.

If the CryptGenKey function is used, it is recommended that the CRYPT_EXPORTABLE parameter be used to create an exportable session key. This creates a value that can be moved from one computer to another. Without this parameter the value returned is only valid on that particular computer/session.

Following is an example of how to use the CryptDeriveKey function, assuming that pPassword points to a user-defined password and dwPasswordLength contains the length of the password.

#include <wincrypt.h>      // CryptoAPI definitions
/*
For non C/C++ users the constants used here are:
#define ALG_CLASS_HASH                  (4 << 13)
#define ALG_TYPE_ANY                    (0)
#define ALG_SID_MD5                     3
#define CALG_MD5        (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5)
#define CRYPT_EXPORTABLE        0x00000001
#define ALG_CLASS_DATA_ENCRYPT          (3 << 13)
#define ALG_TYPE_STREAM                 (4 << 9)
#define ALG_SID_RC2                     2
#define CALG_RC4        (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_STREAM|ALG_SID_RC4)
*/
BOOL bResult;
HCRYPTHASH hHash;
HCRYPTKEY hKey;

// Obtain handle to hash object.
bResult = CryptCreateHash(
            hProv,             // Handle to CSP obtained earlier
            CALG_MD5,          // Hashing algorithm
            0,                 // Non-keyed hash
            0,                 // Should be zero
            &hHash);           // Variable to hold hash object handle 

// Hash data.
bResult = CryptHashData(
            hHash,             // Handle to hash object
            pPassword,         // Pointer to password
            dwPasswordLength,  // Length of data
            0);                // No special flags


// Create key from specified password.
bResult = CryptDeriveKey(
            hProv,               // Handle to CSP obtained earlier.
            CALG_RC4,            // Use a stream cipher.
            hHash,               // Handle to hashed password.
            CRYPT_EXPORTABLE,    // Make key exportable.
            &hKey);              // Variable to hold handle of key.
.
.
.
Use key to do something.
.
.
.
// Release hash object.
CryptDestroyHash(hHash);

// Release key object.
CryptDestroyKey(hKey);

Encrypting and Decrypting Data: CryptEncrypt, CryptDecrypt

It would be easy, although not entirely correct, to say that the Cryptography API revolves around these two functions—the encrypting (CryptEncrypt) and decrypting (CryptDecrypt) of data.

These two functions are extremely useful but require some explanation about their parameters.

  • The first six parameters of each function are the same.
  • The first two parameters are simply handles to the key and an optional hash object.
  • The third parameter is a Boolean that remains FALSE until the last block of data, at which point it must be set to TRUE so that the function can do some special processing for the last block of data.
  • The fourth and fifth parameters are simply a flag value and a pointer to the data to be encrypted or decrypted.
  • The sixth parameter is the number of characters in the buffer to be encrypted.
  • The seventh parameter is usually the same as the sixth parameter in that it specifies how long the block is. This is because for many algorithms the resulting encrypted data is the same length as the decrypted data. However, certain algorithms may increase the length of the encrypted data. In those cases the buffer pointed to by the fifth parameter must be long enough to handle the extra data.

The problem of the longer buffer can be alleviated by using the CryptEncrypt function itself to return the size of the required buffer prior to encryption. This technique is demonstrated in the following sample code. In this sample, certain values are assumed to have been obtained earlier, and we only want to encrypt one buffer of data pointed to by pData, which is dwDataLen bytes in length.

BOOL bResult;
PBYTE pBuffer;
DWORD dwSize;

// Set variable to length of data in buffer.
dwSize = dwDataLen;

// Have API return us the required buffer size.
bResult = CryptEncrypt(
            hKey,            // Key obtained earlier
            0,               // No hashing of data
            TRUE,            // Final or only buffer of data
            0,               // Must be zero
            NULL,            // No data yet, simply return size
            &dwSize,         // Size of data
            dwSize);         // Size of block

// We now have a size for the output buffer, so create buffer.
pBuffer = new char[dwSize];

// Now encrypt data.
bResult = CryptEncrypt(
            hKey,            // Key obtained earlier
            0,               // No hashing of data
            TRUE,            // Final or only buffer of data
            0,               // Must be zero
            pBuffer,         // Data buffer
            &dwSize,         // Size of data
            dwSize);         // Size of block

Encrypting and Decrypting Simultaneously

When encrypting or decrypting two streams of data simultaneously with the same cryptographic key, a certain amount of care must be taken. The same physical session key must not be used for both operations, because every session key contains internal state information and it will get mixed up if used for more than one operation at a time. A fairly simple solution to this problem is to make a copy of the session key. In this way, the original key can be used for one operation and the copy used for the other.

Copying a session key is done by exporting the key with CryptExportKey and then using CryptImportKey to import it back in. When the key is imported, the CSP will give the "new" key its own section of internal memory, as if it were not related at all to the original key.

CRYPTOAPI Sample Application

Overview

The CRYPTOAPI sample application supplied with this article is a "complete" encryption/decryption utility. The application has the capability to add and remove users from the default CSP, to encrypt and decrypt files with or without a password, to sign and verify signatures, and to display the capabilities of the default CSP.

The application has the following command structure.

Usage: Encrypt switch [arguments]

Where switch and optional arguments are one of:

Switch           Arguments        Description
 /A[DDUSER]                       to add user to CSP table
 /R[EMOVEUSER]                    to remove user from CSP table
 /E[NCRYPT]      uf ef [pwd]      to encrypt a file
 /D[ECRYPT]      ef uf [pwd]      to decrypt a file
 /S[IGN]         uf sf [desc]     to sign a file
 /V[ERIFY]       uf sf [desc]     to verify a signed file
 /C[SP]                           to show CSP statistics

 and   uf   = name of an unencrypted file
       ef   = name of an encrypted file
       sf   = name of a signed file
       pwd  = optional password
       desc = optional signature description

Coding Issues

At the time this article was written it was necessary to explicitly define a specific constant in the sample application, because the Cryptography API header file (wincrypt.h) uses the _WIN32_WINNT constant to determine which version of Windows NT® is running. When I wrote the sample application this constant, although necessary for compilation, had not yet been defined by the present compiler. Defining this constant allowed the code to compile without any errors, and the constant can be removed later on when a future compiler defines it.

The CryptAcquireContext API function has an undocumented constant value called MS_DEF_PROV. This constant is used to indicate the default CSP. This value is used in the sample application with the /ADDUSER command line switch. This allows the application to use whatever CSP is installed, without having to know its name.

Adding or Removing a User

The /ADDUSER and /REMOVERUSER switches are used to add or remove the default cryptographic client. The /ADDUSER switch must be run prior to running the other cryptographic functions, in order for them to work correctly.

The following operations are performed:

  1. A default key container is created.
  2. A digital signature key pair is created within the key container.
  3. A key exchange key pair is created within the key container.

These operations only need to be done once, unless the operating system is reinstalled. If the default key container and key pairs have already been created, using this switch again has no effect.

The /ADDUSER switch is run from the command line as follows:

Encrypt /ADDUSER

The /REMOVEUSER switch is run from the command line as follows:

Encrypt /REMOVEUSER

Encrypting or Decrypting a File

The /ENCRYPT switch is used to encrypt files. Files encrypted via this method can be decrypted later with the /DECRYPT switch.

Note that the /ADDUSER switch must be run prior to doing any encryption in order to create a key container for the default user.

The /ENCRYPT switch is run from the command line as follows:

Encrypt /encrypt <source file> <destination file> [ <password> ]

The <source file> argument specifies the filename of the plain-text file to be encrypted, and the <destination file> argument specifies the filename of the cipher-text file to be created. The optional <password> argument specifies a password with which to encrypt the file. If no password is specified, a random session key is used to encrypt the file. This session key is then encrypted with the key exchange public key of the default user and stored with the encrypted file. In this case, the corresponding key exchange private key is later used (by /DECRYPT) to decrypt the session key, which is used in turn to decrypt the file itself.

The /DECRYPT switch is run from the command line as follows:

Encrypt /decrypt <source file> <destination file> [ <password> ]

The <source file> argument specifies the filename of the cipher-text file to be decrypted, and the <destination file> argument specifies the filename of the plain-text file to be created. The optional <password> argument specifies a password with which to decrypt the file. If a bogus password is supplied to /DECRYPT, no error is generated. This is a great security feature because there is no way for someone trying to "break" the data to know whether or not they have done so. Only a legitimate recipient of the data can decrypt it and actually KNOW it was decrypted properly.

Signing and Verifying a File

The /SIGN switch is used to sign files. Files signed with this switch can be later verified with the /VERIFY switch.

Note that the /ADDUSER switch must be run prior to doing any encryption, in order to create a key container for the default user.

The /SIGN switch is run from the command line as follows:

Encrypt /sign <source file> <signature file> <description>

The <source file> argument specifies the filename of the file to be signed, and the <signature file> argument specifies the filename of the file in which to place the signature data. The <description> argument specifies a textual description of the data being signed. This can consist of empty quotes ("") if no description is required. See CryptSignHash in the online documentation for more information on signatures and description strings.

The /VERIFY switch is run with the same arguments as /SIGN. If the contents of the source file, signature file, or description string have changed in any way from when the file was originally signed, an error will be reported.

Showing CSP Statistics

The /CSP switch lists the algorithms supported by the default PROV_RSA_FULL provider. By default, this will be the Microsoft RSA Base Provider, which is included along with the operating system.

Note that the /ADDUSER switch must be run prior to doing any encryption, in order to create a key container for the default user.

The /CSP switch is run from the command line as follows:

Encrypt /csp

In addition to listing the name of each supported algorithm, this switch also lists:

  • The type of algorithm (encryption, hash, key exchange, or signature).
  • The key length used by the algorithm (or number of bits in the hash value for hash algorithms).

The algorithm identifier for the algorithm. This value can be passed to the appropriate Cryptography API function in order to create a key or hash object that makes use of the particular algorithm.

Conclusion

This article has shown you how to encrypt and decrypt files, how to sign and verify files. The Cryptography API can be used to implement and provide a secure environment. With the strength of security that Microsoft has built into the API, I would strongly recommend that you use it for secure messaging but DON'T ever forget the password you used to encrypt your data. If you do, there is no way to get it back. In a future article I will show you how to encrypt data and publicly transmit it over the Internet or any other form of communication. Until then, have fun "keeping a secret!"