<

Easy String Encryption Using CryptoAPI in C++

Published on
25,670 Points
19,570 Views
1 Endorsement
Last Modified:
Approved
When your program needs to access a service, web site, or API that requires a username and password login, how can you store the password so that it is never written to disk in cleartext?  

You need to encrypt it... and I'm not talking about some sort of XOR/scrambling logic that a cypher tech could break in five minutes, but real RSA encryption.  The Windows CryptoAPI provides the tools to do this, but the documentation is rather complicated and the steps are not particularly obvious.

A typical scenario might be that you need to make a database connection that requires a password:

1) Get input of the password used in a SQL Server connection string in a settings dialog.
2) Encrypt: Make the password data unusable except to your program.
3) Store the encrypted data into the System Registry.
   ... later ...
4) Decrypt:  Recover the original cleartext data.
5) Build the database connection string using the cleartext password.

I wrote a smallish (140-line) C++ class to do steps 2 and 4, and I'll provide the entire source code in this article.  Here is the rough outline of the steps:

Setup:
    CryptAcquireContext
    CryptCreateHash
    CryptHashData
    CryptDeriveKey  (create the key needed below)
Encrypt:
    CryptEncrypt
    Convert binary to hexadecimal (for easy transport/storage)
Decrypt:
   Convert hexadecimal to binary (for using the CryptoAPI)
   CryptDecrypt

It boils down to:  Make a CryptoAPI key and use it in calls to CryptEncrypt and CryptDecrypt.   Creating that key is the only tricky (non-obvious) part.  You start with a base raw key (any string of characters), hash it, and then convert that hash into a key that can be used by the CryptoAPI.  

Base Raw Key and Salt
So what do you use as the original base raw key string of characters?  I want my code to be extremely simple and not require that the caller know the base key.  I wanted to be able to use this tool from various program modules transparently, so I provided a built-in "default" base key as a member variable.  You may see the security hole there:  Anyone who has a copy of my program can decrypt anything that has been (default-ly) encrypted by my program.

To give a warm feeling to my clients, I added a feature called a salt -- an optional, user-provided string that can be appended to the default base key string.  Now, if my users add a salt, then anybody who does not know that salt value can never decrypt the passwords (or Social Security Numbers or other encrypted data).  Note:  I put a big warning in my documentation:  "If you forget your salt value, you will lose all of the encrypted data and there is no backdoor.  Don't call tech support because we can't help you."

Problems with Binary Data
One significant problem you will encounter with encrypted data is that it is no longer simple text.   It ends up as a very random-looking array of binary data -- including unprintable characters, apostrophes and percentage signs (SQL users beware!), and worst of all, it may contain embedded binary NULL values (0x00).  As a C/C++ programmer you know that it is possible to work around such situations in various ways, but you also know that you will eventually run into headaches.  

I chose the simple expedient of always storing encrypted data as a C-style string of hexadecimal digits.   The hex-encoded secret data is always exactly twice as long as the encrypted data, which makes it somewhat easier to work with than base-64 encoding or other techniques.

Enough with the jawing... let's get to the code.

Crypt.h -- Header for the Encryption Object
#pragma once
#include "stdafx.h"
#include <Wincrypt.h>

class CCrypt
{
public:
    CCrypt(void);
    virtual ~CCrypt(void) {
        if ( m_hKey )  CryptDestroyKey( m_hKey ); 
        if ( m_hHash ) CryptDestroyHash( m_hHash ); 
        if ( m_hProv ) CryptReleaseContext( m_hProv, 0); 
    }
    BOOL SetKey( LPCSTR szKey= 0, LPCSTR pszSalt= 0 );

    BOOL EncryptDecrypt( BYTE* pData, DWORD* dwDataLen, LPCSTR pKey, BOOL fEncrypt );

    CString EncryptStrToHex(   LPCSTR szText, LPCSTR pKey= 0, LPCSTR pszSalt= 0 );
    CString DecryptStrFromHex( LPCSTR szHex,  LPCSTR pKey= 0, LPCSTR pszSalt= 0 );

    CString EncodeToHex(   BYTE* p, int nLen );
    int     DecodeFromHex( LPCSTR pSrc, BYTE* pDest, int nBufLen );

private:
    HCRYPTPROV  m_hProv;
    HCRYPTHASH  m_hHash;
    HCRYPTKEY   m_hKey;

    BOOL        m_fOK;
    DWORD       m_nLastErr;
    CString     m_sErrMsg;
    char*       m_pszDefaultKeyRaw;
};

Open in new window

Crypt.cpp -- Code of the Encryption Object
#include "utCrypt.h"
CCrypt::CCrypt(void)
{
    m_hProv= m_hHash= m_hKey= 0; 
    m_pszDefaultKeyRaw= "fdC)Y%yum3ww09";
}
BOOL CCrypt::SetKey( LPCSTR szKey, LPCSTR szSalt/*=0*/ )
{
    m_fOK= TRUE;
    if ( 0 == m_hProv ) {
        m_fOK= CryptAcquireContext( &m_hProv, NULL, 
            MS_DEF_PROV, 
            PROV_RSA_FULL, 
            CRYPT_VERIFYCONTEXT 
        );
    }
    if ( m_fOK && (0 != m_hHash) ) {
        m_fOK= CryptDestroyHash( m_hHash ); 
        m_hHash= 0;
    }
    if ( m_fOK && (0 == m_hHash) ) {
        m_fOK= CryptCreateHash( m_hProv, CALG_MD5, 0, 0, &m_hHash );
    }
    if ( m_fOK ) {
        if ( 0 == szKey ) {  // request to use default rawKey
            char szTmp[100];
            strcpy_s( szTmp, sizeof(szTmp), m_pszDefaultKeyRaw );
            if ( szSalt ) {
                strncat_s( szTmp, sizeof(szTmp), szSalt, 5 ); // use part of salt
            }
            // minor security tweak -- scramble the key+salt
            int nLen= strlen(szTmp)-1;  
            for ( int j=0; j< nLen; j++ ) {
                char c= szTmp[nLen-j];
                szTmp[nLen-j]= (char)(szTmp[j]+5);
                szTmp[j]= c;
            }
            szKey= &szTmp[4]; // discard the first part, for fun
        }
        m_fOK= CryptHashData( m_hHash, (BYTE*)szKey, strlen(szKey), 0);
    }
    if ( m_fOK ) {
        m_fOK= CryptDeriveKey( m_hProv, CALG_RC4, m_hHash, CRYPT_EXPORTABLE, &m_hKey);
    }
    if ( !m_fOK ) { 
        m_nLastErr= GetLastError(); 
        m_sErrMsg= "Error creating encryption key";
    }
    return( m_fOK );
}
//--- workhorse function:  Encrypt or decrypt "in place"
BOOL CCrypt::EncryptDecrypt( BYTE* pData, DWORD* dwDataLen, LPCSTR pKey, BOOL fEncrypt )
{
    m_fOK= TRUE;
    SetKey( (LPCSTR)pKey );	
    if ( fEncrypt ) {
           m_fOK= CryptEncrypt( m_hKey, 0, TRUE, 0, pData, dwDataLen, *dwDataLen );
    }
    else  {
        m_fOK= CryptDecrypt( m_hKey, 0, TRUE, 0, pData, dwDataLen );
    }
    return( m_fOK );
}

CString CCrypt::EncryptStrToHex( LPCSTR szText, LPCSTR pszKey/*= 0*/, LPCSTR pszSalt/*= 0*/ )
{
    m_fOK= TRUE;
    CString sRet= "";
    DWORD nDataLen= strlen( szText );
    if ( pszSalt || pszKey || (0 == m_hKey) ) {
        m_fOK= SetKey( (LPCSTR)pszKey, pszSalt );	
    }
    if ( m_fOK ) {
        char* pTmp= new char[nDataLen+1] ;
        strncpy_s( pTmp, nDataLen+1, szText, nDataLen+1 );
        m_fOK= CryptEncrypt( m_hKey, 0, TRUE, 0, (BYTE*)pTmp, &nDataLen, nDataLen );
        if (m_fOK ) {
            sRet= EncodeToHex( (BYTE*)pTmp, nDataLen );
        }
        delete pTmp;
    }
    return( sRet );
}

CString CCrypt::DecryptStrFromHex( LPCSTR szHex, LPCSTR pszKey/*=0*/, LPCSTR pszSalt/*= 0*/ )
{
    m_fOK= TRUE;
    CString sRet= "";
    DWORD nDataLen= strlen( szHex );

    if ( pszSalt || pszKey || (0 == m_hKey) ) {
        m_fOK= SetKey( (LPCSTR)pszKey, pszSalt );	
    }
    if ( m_fOK ) {
        DWORD nDecryptLen= nDataLen/2;
        char* pTmp= new char[ nDecryptLen+1 ];
        DecodeFromHex( szHex, (BYTE*)pTmp, nDecryptLen );
        m_fOK= CryptDecrypt( m_hKey, 0, TRUE, 0, (BYTE*)pTmp, &nDecryptLen );
        if ( m_fOK ) {
            sRet= pTmp;
        }
        delete pTmp;
    }
    return( sRet );
}

//--------------------------------------------------------
// inefficient but requires no explanation :-)
CString CCrypt::EncodeToHex( BYTE* p, int nLen )
{
    CString sRet, sTmp;
    for( int j=0; j< nLen; j++ ) {
        sTmp.Format( "%02x", p[j] );
        sRet+= sTmp;
    }
    return (sRet );
}

//---------------------------------------------------------
// returns length of decoded hex buffer
int CCrypt::DecodeFromHex( LPCSTR pSrc, BYTE* pDest, int nBufLen )
{
    int nRet= 0;
    int nLen= strlen(pSrc);
    *pDest = 0;
    BYTE cIn1, cIn2, nFinal;
    for( int j=0; j< nLen; j += 2 ) {
        cIn1= (BYTE)toupper(*pSrc++);  cIn1 -= '0'; if ( cIn1>9 ) cIn1 -= 7;
        cIn2= (BYTE)toupper(*pSrc++);  cIn2 -= '0'; if ( cIn2>9 ) cIn2 -= 7;
        nFinal= (BYTE)((cIn1 << 4) | cIn2); 
        if (nFinal>255) nFinal=0; // in case trying to decode non-hex data
        *pDest++ = nFinal; 
        *pDest = 0;
        if ( nRet >= nBufLen ) {
            break;
        }
        nRet++;
    }
    return( nRet );
}

Open in new window


Here is an example of usage:
#include "Crypt.h"
    ...
void CEncryptTestDlg::OnBnClickedButton1()
{
   CCrypt crypt;
   char szSrc[]="Secret Password";

   //------------ use default rawKey
   CString sEncrypted= crypt.EncryptStrToHex( szSrc );  
   CString sDecrypted= crypt.DecryptStrFromHex( sEncrypted );
   MessageBox( sEncrypted,sDecrypted );

   //------------ test use of wrong salt value
   sEncrypted= crypt.EncryptStrToHex(   szSrc,     0,"mySalt"   );  
   sDecrypted= crypt.DecryptStrFromHex( sEncrypted,0,"yourSalt" ); // oops!
}

Open in new window


Notes:
The EncryptStrToHex function assumes that the input data is simple text -- it calculates the length using strlen().  Don't use it to encrypt binary data (any data that might contain an embedded NULL).
The CryptEncrypt and CryptDecrypt CryptoAPIs do their work in place; that is, they overwrite the original with the modified data.  I wanted to be able to pass LPCSTR data (constant string) to the functions, so I chose to make a copy of the incoming data.  This would be inefficient if working with larger buffers.
My actual (production) code provides a few variations of the main functions; for instance, it can encrypt binary data (not just text strings) and one variation does not encode to hex.  I removed most of this extraneous code from this article because those functions are not needed nearly as often -- and I wanted to leave something for you to do on your own :-)
You may want to "disguise" the length of the secret data.  For instance, a codebreaker could save a lot of time knowing that the cleartext string is only 3 characters (six hexadecimal digits).   The simplest technique is to pad the input string with spaces, and then strip them off when the cleartext is needed.  

But you can also use the lower-level Crypt::EncryptDecrypt function -- it does not assume that the input is clean text, so it will accept embedded NULLs; thus, you could append a NULL and pad with random characters.  For instance, if the input text is
     "Secret Password\0asXXefg"
then after decrypting, the string will end at the right place.
Just an afterthought:  If all you need is a way to authenticate the person who uses your program, there is no need to encrypt or store or transport his password at all.  Instead just create and store a hash of his password.  Each time he logs on, hash the input password and compare it to the stored value.  That way the cleartext password is irrelevant -- never needed and never stored.  

References:
Cryptography Reference
http://msdn.microsoft.com/en-us/library/aa380256(VS.85).aspx

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
If you liked this article and want to see more from this author,  please click the Yes button near the:
      Was this article helpful?
label that is just below and to the right of this text.   Thanks!
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
1
Author:DanRollins
Ask questions about what you read
If you have a question about something within an article, you can receive help directly from the article author. Experts Exchange article authors are available to answer questions and further the discussion.
Get 7 days free