Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Cryptography using the Win32 Cryptography API

9 Aug 2003 0  
An ATL COM Object Demonstrating How-to Use the Win32 Crypto API

Sample Image - ContextMenu.jpg

Overview

The purpose of this article is to demonstrate how easy it is to combine the Win32 Crypto API with a simple Windows Explorer context menu shell extension to give users a way to quickly encrypt and decrypt files through the windows shell. The Source code implements an in-proc COM server that provides two interfaces:

  • ICrypto: Provides general encryption/decryption and Base64 encoding/decoding methods
  • ICryptoShellExt: Uses ICrypto to extend the Windows Explorer context menu to provide four menu items shown in the screen shot above

I wrapped the Win32 cryptography behavior and Base64 encoding/decoding in a COM interface named ICrypto (Crypto.h/Crypto.cpp in the Source). The Base64 encoding/decoding implementation was taken from the Microsoft SOAP SDK. The only modification that I made was a slight enhancement to the decoding algorithm to increase decoding performance. ICryptoShellExt implements the shell extension interfaces necessary for extending the Windows Context menu. ICrypto provides general methods for encrypting/decrypting files, Base64 encoding/decoding and generating digital signatures.

Supported Platforms:

Windows '95 OSR2 (Internet Explorer 3.02 and higher)
Windows '98
Windows ME
Windows NT
Windows 2000

Full support for UNICODE

Win32 Crypto API Functions Used:

CryptCreateHash
CryptEncrypt
CryptDestroyKey
CryptDestroyHash
CryptDecrypt
CryptImportKey
CryptReleaseContext
CryptAcquireContext
CryptGetUserKey
CryptGenKey
CryptExportKey
CryptSignHash

ICrypto Methods:

    
    HRESULT 
    EncryptDoc( [in] BSTR bstrSrc, [in, optional] VARIANT varDestination );    
        // Description: Encrypts a file and stores the encrypted results 

        // in either another file or overwrites the same file


                          
    HRESULT 
    DecryptDoc( [in] BSTR bstrSrc, [in, optional] VARIANT varDestination );
        // Description: Decrypts a file and stores the decrypted results 

        // in either another file or overwrites the same file

                

    HRESULT 
    Base64EncodeString( [in] BSTR bstrSrc, [out, retval] BSTR* pbstrResult );    
        // Description: Base64 encodes the argument string and returns the 

        // result through the argument pbstrResult

    
    
    HRESULT 
    Base64DecodeString( [in] BSTR bstrSrc, [out, retval] BSTR* pbstrResult );    
        // Description: Base64 decodes the argument string and returns 

        // the result through the argument pbstrResult

        

    HRESULT 
    EncryptString( [in] BSTR bstrSrc, [out, retval]    BSTR* pbstrResult );    
        // Description: Encrypts the argument string and returns the result 

        // through the argument pbstrResult



    HRESULT 
    DecryptString( [in] BSTR bstrSrc, [out, retval]    BSTR* pbstrResult );
        // Description: Decrypts the argument string and returns the result 

        // through the argument pbstrResult

    

    HRESULT 
    VerifyDigitalSignature( [in] BSTR bstrDigSig, 
                            [out, retval] BOOL* pbMatches );
        // Description: Returns whether the last encryption/decryption 

        // operation generated the same digital signature as the argument 

        // through pbMatches.



    HRESULT 
    Base64EncodeFile( [in] BSTR bstrSrc, 
                      [in, optional] VARIANT varDestination );    
        // Description: Base64 encodes a file and stores the encoded results 

        // in either another file or overwrites the same file.



    HRESULT 
    Base64DecodeFile(  [in] BSTR bstrSrc, 
                       [in, optional] VARIANT varDestination );
        // Description: Base64 decodes a file and stores the decoded results 

        // in either another file or overwrites the same file.



    HRESULT 
    Base64EncodeFileToString( [in] BSTR bstrSrc, 
                              [out, retval] BSTR* pbstrResult );
        // Description: Base64 encodes a file and stores the encoded results

        // in the argument string



    HRESULT 
    Base64DecodeFileToString( [in] BSTR bstrSrc, 
                              [out, retval] BSTR* pbstrResult );
        // Description: Base64 decodes a file and stores the decoded results

        // in the argument string    

    
    

ICrypto Properties:

    
    HRESULT 
    DigitalSignature( [out, retval] BSTR* pbstrResult );    
        // Description: Returns the digital signature generated from the 

        // last encryption/decryption operation Base64 encoded

        
        
    HRESULT 
    get_ContainerName( [out, retval] BSTR* pbstrResult );    
        // Description: Returns the key container name for the CSP 

        // (Cryptographic Service Provider). 

        // This container name property should be an application unique 

        // string identifying the container that the CSP will use for storing

        // the keys



    HRESULT 
    put_ContainerName( [in] BSTR bstrContainerName );    
        // Description: Sets the key container name for the CSP 

        // (Cryptographic Service Provider). 

        // This container name property should be an application unique 

        // string identifying the container that the CSP will use for 

        // storing the keys

    
    

Digital Signatures

Digital signatures are calculated when the data is being encrypted using what the Crypto API documentation refers to as a hash. I maintain the hash object internally before doing any encryption or decryption. Once the encryption/decryption is complete and the digital signature is calculated, ICrypto provides access to it via the property named DigitalSignature. The DigitalSignature is stored internally as a BLOB. However, when a client requests the Digital Signature ICrypto passes it out as a Base64 encoded string (to ease use for scripting clients). Scripting clients typically have a difficult time with binary data (sometimes handled as a SAFEARRAY of variants).
Typically, you would perform the encryption on a file or memory, then immediate get the Digital Signature property from ICrypto and store it. Later when you decrypt the file or memory you would call VerifyDigitalSignature to determine if the digital signature matches (if the contents of the file or memory has changed since you encrypted).

ContainerName Property

The container name should be an application unique identifier for the key container. ICrypto uses this property when acquiring a handle to the context (CryptAcquireContext). If you fail to provide a container name, then ICrypto uses its own container name. Here's some code taken from the ICrypto implementation that shows how the container name is used during initialization:

BOOL CCrypto::InitializeContainer() const 
    { 
    USES_CONVERSION; 
    BOOL bSuccess = FALSE; 
    int nStringID= IDS_UNKNOWN_ERROR; 

    
    ATLASSERT( !m_hContext ); 
    bSuccess = CryptAcquireContext( &m_hContext, 
              W2T(m_bstrContainerName.m_str), MS_DEF_PROV, PROV_RSA_FULL, 0 ); 
    
     // Container doesn't exist - Possibly try to create a new container 

    if( !bSuccess ) 
        { 
        int nLastError = GetLastError(); 
        
        if( nLastError == NTE_BAD_KEYSET ) 
            { 
                // Creating the key container for the first time 

            bSuccess = CryptAcquireContext( &m_hContext, 
                          W2T(m_bstrContainerName.m_str), MS_DEF_PROV, 
                          PROV_RSA_FULL, CRYPT_NEWKEYSET ); 
            if( !bSuccess ) 
             nStringID = IDS_ERROR_INITIALIZATION_CREATE_NEW_CONTAINER_FAILED; 
            } 
        else nStringID = LookupCryptErrorStringID( nLastError ); 
        } 
    if( !bSuccess ) 
        CRYPTO_OUTPUT_DEBUGSTRING( nStringID ); 
    
    return bSuccess; 
    }  

Initialization

Once I "finished" the first revision of the implementation I decided it was time to start testing on various Windows platforms (other than my production machine). The first platform that I tried was a clean machine running Windows '98 SP1. The test harness immediately failed. After several hours of debugging and reading, I discovered an important article in MSDN regarding initialization of the key container. I found a sample in the knowledge base for "best practices" for initialization and basically cut-and-pasted that sample into ICrypto. Be sure to see how CCrypto::Initialize() works if you plan to use the Win32 Crypto API.

Samples:

The following code samples demonstrate how to use ICrypto with a C++ and Java Script client. See also CryptoShellExt.cpp in the Source code for another sample in ATL/C++.

ICrypto C++ Client

How to use ICrypto with a C++ Client:

Steps:

  1. Download the source, unzip it to a folder and build the project
  2. Be sure the CryptoAPI.dll is located somewhere in your include path 
  3. Import the type information for ICrypto by inserting the following code in the source file in your project that is going to use ICrypto:
    #import "CryptoAPI.DLL" named_guids raw_interfaces_only
  4. Create the instance of ICrypto and initialize the container name for your application (for more information about Cryptographic Service Provider key container names, see: http://msdn.microsoft.com/library/psdk/crypto/cryptoref1_0wvo.htm)
    .
    .
    CComPtr<ICrypto> pCrypto;
    
    if( SUCCEEDED( pCrypto.CoCreateInstance( CLSID_Crypto ) ) )
       {
       ATLASSERT( pCrypto != NULL );
       if( pCrypto )
          {
          pCrypto->put_ContainerName( L"MY_APPLICATION_CSP_CONTAINER_NAME" );
    .
    .
    .  
  5. Use the functions in the API to encrypt/decrypt files:
    if( SUCCEEDED( pCrypto->EncryptDoc( bstrFileName, vtMissing ) )
       {
          // Grab the digital signature to verify the file during decryption
    
          // later
    
       pCrypto->get_DigitalSignature( &bstrDigitalSignature );
       }
    else
       {
       IErrorInfo* pErrorInfo = NULL;
       if( ::GetErrorInfo( 0, &pErrorInfo ) == S_OK &&  pErrorInfo  != NULL) 
        {
        CComBSTR    bstrErrorMsg;
            
        pErrorInfo->GetDescription( &bstrErrorMsg );
        ::MessageBox( NULL, _bstr_t(bstrErrorMsg.m_str), NULL, 
                      MB_OK | MB_ICONEXCLAMATION );
        pErrorInfo->Release();
        }       
       }

ICrypto Java Script Client

How to use ICrypto with a Java Script Client:

Encrypting a File

function OnEncryptFile()
   {
   try
      {
      var Crypto = new ActiveXObject( "CryptoAPI.Crypto" );

      Crypto.ContainerName = "MY_APPLICATION_CSP_CONTAINER_NAME";
      Crypto.EncryptDoc( g_strFileToEncrypt );
      g_strDigitalSignature = Crypto.DigitalSignature;
      delete Crypto;
      }
   catch( exception )
      {
      window.alert( exception.description );
      }
  

Verifying the Digital Signature

function OnVerifySignature()
   {
   try
      {
      var Crypto = new ActiveXObject( "CryptoAPI.Crypto" );

      Crypto.ContainerName = "MY_APPLICATION_CSP_CONTAINER_NAME";
      Crypto.DecryptDoc( g_strFileToDecrypt );
      if( !Crypto.VerifyDigitalSignature( g_strDigitalSignature ) )
         {
         // The digital signature doesn't match - The file has been modified

         .
         .
         .
         }
      delete Crypto;
      }
   catch( exception )
      {
      window.alert( exception.description );
      }

In-memory Encryption Binary Format

 

File Encryption Binary Format

 

ICryptoShellExt

Guarding Against Multiple Encryptions

Guarding against the user encrypting a file more than once was an interesting problem to solve. CryptEncrypt returns NTE_DOUBLE_ENCRYPT when you try to encrypt data more than once. However, since ICrypto adds other information to the file (i.e. version, key size) it would need to try and extract the encrypted data and run it through CryptEncrypt to check the return code.

After coming up with a couple of other alternatives (that I didn't like), I decided to run the problem by one of my colleagues. He immediately came up with multiple solutions. One of which I used in ICryptoShellExt. Before encrypting a file, he suggested that I attempt to decrypt the file to a temp file. If the decryption was indeed successful, then the file was already encrypted. Otherwise, the file indeed needed to be encrypted for the first time. Here is the code that handles this case in ICryptoShellExt:

.
.
.

//    Change to hour glass icon

HCURSOR    hCursor = SetCursor( LoadCursor( NULL, IDC_WAIT ) );
switch( Action )
    {
    case CCryptoShellExt::cryptEncrypt:
        {
        USES_CONVERSION;
        _variant_t    varDblEncryptFilename = GetDblCryptTempFilename();

        //    Try to decrypt first to guard against multiple encryptions

        if( FAILED( pCrypto->DecryptDoc( m_pbstrFiles[ nIndex ].m_str, 
                                         varDblEncryptFilename ) ) )
        {
            ( lpctstrDestination != NULL ) ?
                CHECK_HR( pCrypto->EncryptDoc( 
               m_pbstrFiles[ nIndex ].m_str, _variant_t(bstrDestination) ) ):
                CHECK_HR( pCrypto->EncryptDoc( m_pbstrFiles[ nIndex ].m_str, 
                          vtMissing ) );
            }

            //    Cleanup the intermediate file

            _unlink( W2A(varDblEncryptFilename.bstrVal) );                                
            break;
        }
.
.
.

Further Enhancements

The shell extension could easily be extended to include a quick view feature to display the decrypted contents using the associated viewer associated with the file type using ShellExecute(...).

There are several samples in MSDN that demonstrate how to hash in a password during encryption. The shell extension and ICrypto implementations could be enhanced to provide a way to password protect encrypted files. The examples show how to hash in a password as part of the signed hash.

Referring to the in-memory binary format, the private key is included in the encrypted results which increases the size of the result by approximately 76 bytes (typical size of the encrypted private key). Splitting the private key from the in-memory encrypted string could be accomplished by changing the ICrypto interface to return the encrypted string and private key separately. 

ICrypto could be enhanced to include properties for setting the CSP algorithm used for hashing and encrypting block and stream ciphers. It currently uses CALG_RC2 for block encryption, CALG_RC4 for stream encryption and CALG_MD5 for hashing. See MSDN for more information about the different algorithms supported by the Win32 crypto API.

ICrypto could be enhanced to include properties for tweaking the number of bytes read from a file at-a-time. For large files, the implementation could be altered to incorporate caching and reading larger blocks of data for enhancing performance. The current implementation reads in 512 bytes at-a-time.

Reporting Defects and Suggestions

Please report all defects to me by email: mailto:slater_chad@hotmail.com

Please also feel free to submit suggestions and comments. I wrote this article in hopes to help others and to gain feedback from other developers. Enjoy!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here