Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

An AES Encrypting Registry Class

4.86/5 (31 votes)
17 Nov 2006CPOL7 min read 2   8.8K  
This article presents yet another registry class which encrypts data using AES.

Introduction

Screenshot - image02.png

The CAESEncRegKey class addresses the occasional problem of securely saving data to the registry. This article presents yet another registry class which encrypts data using AES. The class presented here uses AES or the Advanced Encryption Standard in CBC Mode.

Currently, NIST has approved three symmetric encryption algorithms for use in Federal processing: AES, Triple DES, and Skipjack. AES is specified in FIPS-197. AES was originally known as Rijndeal (pronounced "Rhine dahl"), which was the work of Joan Daemen and Vincent Rijmen — hence the portmanteau Rijndael. AES is a block cipher that accepts key varying lengths.

The DES algorithm was introduced in 1976 by FIPS-46. In 1988 DES underwent it's first revision, resulting in FIPS-46-1. FIPS-46-2 followed in 1993. Triple DES (TDES) was approved in 1998 in FIPS-46-3. Then in May 2005, DES and Triple DES were withdrawn as standards. Due to popularity in Federal circles, Triple DES received a stay of execution until 2030 in NIST SP800-67.

Finally Skipjack is specified in FIPS-185. FIPS-185 specifies the Escrowed Encryption Standard. FIPS-185 is the framework which allows decryption of an encrypted telecommunication stream intercepted by law enforcement or other jurisdictional agency.

Image 2Crypto++

The underlying cryptographic library is Wei Dai's Crypto++. For more information on using Crypto++, see Integrating Crypto++ into the Microsoft Visual C++ Environment. If you are new to the library, please take the time to familiarize yourself. Other cryptographic libraries exist, such as Peter Guttman's Cryptlib. The reader is encouraged to modify the program presented in this article to include other libraries.

Background

Windows maintains a secure area of the Registry called the SAM (Security Accounts Manager). Users, administrators, and programmers are generally not permitted access to this area of the registry directly. One must use API functions such as the LSA family; or tools such as User Manager for Domains or Active Directory users and computers. The SAM contains information such as cached user login information and local accounts.

Image 3

To allow programmers to securely save data to the registry (but not the SAM), a programmer can use CAESEncRegKey. However, there are a few limitations that one must observe when using the registry. The most important for the purposes of this article is limiting binary data (value type REG_BINARY) size at or below 2048 bytes. See Microsoft's Registry Element Size Limit in MSDN.

Using the built in Windows Scheduler (at.exe) from the command line, we schedule a launch of regedit.exe so that we can explore the SAM tree. Because the scheduler runs under NT_AUTHORITY, regedit will be launched with built in System rights and privileges.

Image 4

Downloads

This article has four downloads. The GUI demo is a release build demonstration which exercises the CAESEncRegKey class. The CLI demo is an AES proof of concept using the Crypto++ library. It simply demonstrates the Crypto++ AES encryption/decryption process.

The key and IV generator program uses the Crypto++ AutoSeededRandomPool PRNG to create pseudo random values for the key and IV vectors. For guidelines on random number generator usage, see A Survey of Pseudo Random Number Generators.

Image 5

The final sample exercises the encrypting registry class.

Image 6

Advanced Encryption Standard

AES was standardized by NIST in November 2001. AES is a 128 bit block cipher that accepts key lengths of 128, 192, and 256 bits. There are seven modes of operation approved for AES. Modes of operation specify how the output of one stage (for example stage i) is used as input to the subsequent stage (for example, i+1). Because all modes (except ECB) are used in a feedback mechanism, modes require an Intitialization Vector: at iteration 0, there is no feedback from iteration -1.

FIPS-81 and SP800-38A provides five modes for confidentiality: ECB, CBC, CFB, OFB, and CTR. Confidentiality is what generally we think of when we refer to encryption. Authentication is provided by CMAC specified in SP800-38A. CCM mode of SP800-38C is used when both confidentiality and authentication are desired (CCM is sometimes referred to as CBC-MAC).

Finally, ANSI X9.52 specifies seven modes. Four modes are equivalent to FIPS-81: ECB, CBC, CFB, and OFB modes. The remaining three modes in are variants of the CBC, CFB, and OFB modes.

Crypto++ Encryption and Decryption Process

Please see Applied Crypto++: Block Ciphers for a complete discussion of the library's implementation of symmetric encryption algorithms. The article is an indepth look at StreamTransformationFilters, TransformationFilters, and Modes of Operation.

By using a filter attached to a Crypto++ Encryption or Decryption object, we are shielded from the buffering and padding issues associated with a block cipher such as AES.

C++
// Key Setup

byte key[ CryptoPP::AES::DEFAULT_KEYLENGTH ];
byte iv[ CryptoPP::AES::BLOCKSIZE ];
::memset( key, 0x01, CryptoPP::AES::DEFAULT_KEYLENGTH );
::memset( iv, 0x01, CryptoPP::AES::BLOCKSIZE );

// Message M

std::string PlainText = "Abraham Lincoln said, 'In the end, "
  "it's not the years in your life that count. It's the life in your years.'";
 
// Encryptor

CryptoPP::CFB_Mode< CryptoPP::AES >::Encryption 
  Encryptor( key, sizeof(key), iv);
 
// Encryption

CryptoPP::StringSource( PlainText, true,
  new CryptoPP::StreamTransformationFilter(
    Encryptor,
    new CryptoPP::StringSink( CipherText )
  ) // StreamTransformationFilter

); // StringSource

 
 
// Decryptor

CryptoPP::ECB_Mode< CryptoPP::AES >::Decryption
  Decryptor( key, sizeof(key),iv );

// Decryption

CryptoPP::StringSource( CipherText, true,
  new CryptoPP::StreamTransformationFilter(
    Decryptor,
    new CryptoPP::StringSink( RecoveredText )
  ) // StreamTransformationFilter

); // StringSource


// RecoveredText is ready for use 

CAESEncRegKey Class

The CAESEncRegKey includes member functions for reading and writing of encrypted and unencrypted data. The class adds an additional parameter to the standard API, allowing us to specify if we would like to encrypt the data. To have a minimal impact on existing source code, the boolean value has a default value of false.

The supported registry value types are REG_BINARY, REG_DWORD, and REG_SZ. The following will discuss the operation of writing and reading of an encrypted string. The class contains data members for the stateful representation of the following.

  • AES Key
  • AES IV
  • HKEY
  • SubKey
  • ValueName

Generally, most will not change once set. The exception is ValueName, which changes as multiple data is read and written. AES Key and AES IV are struct to simplify operations. SubKey and ValueName are CStrings. In the spirit of Microsoft Registry APIs, all functions have a return value type of LONG.

CAESEncRegKey Encryption

Below is the WriteString() function. It will defer to WriteEncString() or WriteNonEncString() as required.

C++
LONG CAESEncRegKey::WriteString(LPCTSTR pszData, 
                       BOOL bEncrypt /*=FALSE*/) const
{
  LONG lResult = ERROR_SUCCESS;
 
  if( TRUE == bEncrypt ) {
  
    lResult = WriteEncString( pszData );
    
  } else {
  
    lResult = WriteNonEncString( pszData );
  }
  
  return lResult;
}

WriteNonEncString() simply acts like any other registry class - it writes the string to the registry. RegCreateKeyEx() is used to open keys for writing, since RegCreateKeyEx() will either create the key or open an existing key. WriteEncString() is coded as follows. Note that two operations occur:

  1. the string is encrypted using EncryptData(),
  2. the string is written to the registry using WriteNonEncBinary().
C++
LONG CAESEncRegKey::WriteEncString(LPCTSTR pszData) const
{
  LONG lResult = ERROR_SUCCESS;
    
  // Returned from EncryptData()

  BYTE* pcbEncryptedData = NULL;
  DWORD dwEncryptedSize = 0;

  lResult = EncryptData( reinterpret_cast<const BYTE*>(pszData),
                    ( ::lstrlen( pszData ) + 1 ) * sizeof( TCHAR ),
                    &pcbEncryptedData, &dwEncryptedSize );
  // Sanity Check

  if( ERROR_SUCCESS != lResult ) { goto FINISHED; }

  // Save it

  lResult = WriteNonEncBinary( pcbEncryptedData, 
                                     dwEncryptedSize );

FINISHED:

  // Cleanup

  if( NULL != pcbEncryptedData ) { delete[] pcbEncryptedData; }

  return lResult;
}

WriteNonEncBinary() performs the registry write, as if it were simply called to write unencrypted binary data. WriteNonEncBinary() will be called frequently, since writing data that has already been encrypted is same as writing non-encrypted data.

C++
LONG CAESEncRegKey::WriteNonEncBinary(const BYTE *pcbData, 
                                            UINT nSize) const
{
  LONG lResult = ERROR_SUCCESS;
  HKEY hKey    = NULL;
  
  // When writing, create the key if it does not exist

  lResult = RegCreateKeyEx( _hKey, _szSubKey, 0, NULL,
                          REG_OPTION_NON_VOLATILE,
                          KEY_WRITE, NULL, &hKey, NULL );

  // Sanity Check

  if( ERROR_SUCCESS != lResult ) { goto FINISHED; }

  // Paydirt

  lResult = RegSetValueEx(hKey, _szValueName, 0, REG_BINARY, 
             reinterpret_cast<const BYTE*>( pcbData ), nSize);
FINISHED:

  // Cleanup

  if( NULL != hKey ) { RegCloseKey( hKey ); }

  return lResult;
}

CAESEncRegKey Decryption

The ReadEncString() function calls ReadData(), which returns the binary data and size of the encrypted string. It is ReadEncString()'s responsibility to free it. Also note that ReadData() opens the key using RegOpenKeyEx() — the key is not created if missing.

Once the encrypted data is available, DecryptData() is called. Again, the DecryptData() function simply decrypts the data using the AES::Decryption object.

C++
LONG CAESEncRegKey::ReadEncString(CString &szValue) const
{
  LONG lResult = ERROR_SUCCESS;

  // Returned from the Registry through ReadData(...)

  DWORD    dwSize  = 0;
  BYTE*    pcbData = NULL;

  // Returned from DecryptData(...)

  DWORD    dwDecryptedSize  = 0;
  BYTE*    pcbDecryptedData = NULL;
  
  // Read From the Registry

  lResult = ReadData( &pcbData, &dwSize );

  // Sanity Check

  if( ERROR_SUCCESS != lResult ) { goto FINISHED; }

  // Undo it...

  lResult = DecryptData( pcbData, dwSize, 
              &pcbDecryptedData, &dwDecryptedSize );

  // Sanity Check

  if( ERROR_SUCCESS != lResult ) { goto FINISHED; }

  // Paydirt...

  szValue = 
    reinterpret_cast<const TCHAR*>( pcbDecryptedData );

FINISHED:

  // Cleanup...

  if( NULL != pcbData ) { delete[] pcbData; }
  if( NULL != pcbDecryptedData ) { delete[] pcbDecryptedData; }

  return lResult;
}

Finally, when reading data, CAESEncRegKey mimics the functionality of RegQueryValueEx(...) by returning ERROR_SUCCESS or ERROR_MORE_DATA depending on the cbBuffer and cbSize arguments. See RegQueryValueEx in MSDN for a detailed explanation. Below is a sample from ReadEncBinary(...). Note that the data must be read from the registry, and then decrypted before the buffer size can be determined.

C++
LONG CAESEncRegKey::ReadEncBinary(BYTE* pcbData, 
                                  DWORD* dwSize) const {
    ...

    // Read From the Registry

    lResult = ReadData( &pcbRegistryData, &dwRegistrySize );

    ...

    // Just Undo It...

    lResult = 
       DecryptData( pcbRegistryData, dwRegistrySize, &pcbDecryptedData,
           &dwDecryptedSize );
    ...

    // Emulate the RegQueryValue(...) Function

    if( *dwSize < dwDecryptedSize && NULL != pcbData ) {

        lResult = ERROR_MORE_DATA;
        *dwSize = dwDecryptedSize;
        goto FINISHED;
    }

    // Emulate the RegQueryValue(...) Function

    if( *dwSize < dwDecryptedSize && NULL == pcbData ) {

        lResult = ERROR_SUCCESS;
        *dwSize = dwDecryptedSize;
        goto FINISHED;
    }

    // Inform Caller of size of pcbData

    *dwSize = dwDecryptedSize;

    ...

    return lResult;
}

Using CAESEncRegKey

CAESEncRegKey not only provides functions for reading and writing of registry value types, it also provides mutators and accessors for changing the registry key object. As stated earlier, two helper structures are used to manage the AES Key and AES IV. The structures are declared and defined in aeshelper.h.

The following three examples show object construction:

C++
// Create an default object

CAESEncRegKey aesKey;
// Create an object setting Registry paths

CAESEncRegKey aesKey(HKEY_LOCAL_MACHINE,
      _T("\\Software\\Code Guru"), _T("Value Name"));
// Create an object setting Key and IV vectors

BYTE key[ AES::DEFAULT_KEYLENGTH ] = { 0x00, 0x01, ... , 0x0E, 0x0F };
BYTE  iv[ AES::BLOCKSIZE ] = { 0x0F, 0x0E, ... , 0x01, 0x00 };
CAESEncRegKey aesKey( key, AES::DEFAULT_KEYLENGTH, iv, AES::BLOCKSIZE );

If using the default constructor, set the five data members before invoking a WriteXXX(...) function, since the default constructor does not perform any state initialization.

C++
CAESEncRegKey aesKey;
BYTE key[ 16 ] = { 0x00, 0x01, ... , 0x0E, 0x0F };
BYTE  iv[ 16 ] = { 0x0F, 0x0E, ... , 0x01, 0x00 };

aesKey.SetKey( key, CryptoPP::AES::DEFAULT_KEYLENGTH );
aesKey.SetIV (  iv, CryptoPP::AES::BLOCKSIZE );

aesKey.Set.SetHKEY( GetHKey() );

aesKey.SetSubKey( GetSubKey() );
aesKey.SetValueName( GetStringValueName() );

aesKey.WriteString( "...", TRUE );

Reading data from the registry is simply the reverse operation:

C++
CAESEncRegKey aesKey;
BYTE key[ AES::DEFAULT_KEYLENGTH ] = { 0x00, 0x01, ... , 0x0E, 0x0F };
BYTE  iv[ AES::BLOCKSIZE ] = { 0x0F, 0x0E, ... , 0x01, 0x00 };

aesKey.SetKey( key, AES::DEFAULT_KEYLENGTH );
aesKey.SetIV (  iv, AES::BLOCKSIZE );

aesKey.SetHKEY( GetHKey() );

aesKey.SetSubKey( GetSubKey() );
aesKey.SetValueName( GetStringValueName() );

CString szData;

aesKey.ReadString( szData, TRUE );

Reading multiple key values would be accomplished as follows:

C++
CAESEncRegKey aesKey;
CString szData;
BYTE key[ AES::DEFAULT_KEYLENGTH ] = { 0x00, 0x01, ... , 0x0E, 0x0F };
BYTE  iv[ AES::BLOCKSIZE ] = { 0x0F, 0x0E, ... , 0x01, 0x00 };

...

aesKey.SetValueName( _T("Value 1") );
aesKey.ReadString( szData, TRUE );
// Do something with retrieved data


aesKey.SetValueName( _T("Value 2") );
aesKey.ReadString( szData, TRUE );
// Do something with retrieved data


aesKey.SetValueName( _T("Value 3") );
aesKey.ReadString( szData, TRUE );
// Do something with retrieved data

Finally, the code from the driver program which verifies reading and writing of encrypted binary data:

C++
void CRegistryEncryptionDlg::OnTestBinary() 
{
  BYTE data[ MAX_REG_BINARY_SIZE ];
  CryptoPP::AutoSeededRandomPool rng;
  
  rng.GenerateBlock( data, MAX_REG_BINARY_SIZE );
  
  _AESEncRegKey.SetHKEY( GetHKey() );
  _AESEncRegKey.SetSubKey( GetSubKey() );
  _AESEncRegKey.SetValueName( GetBinaryValueName() );
  
  _AESEncRegKey.WriteBinary(data, MAX_REG_BINARY_SIZE, TRUE);
  
  DWORD dwSize = MAX_REG_BINARY_SIZE;
  BYTE  decrypted[ MAX_REG_BINARY_SIZE ];
  
  _AESEncRegKey.ReadBinary( decrypted, &dwSize, TRUE );
  
  if( 0 == memcmp( data, decrypted, MAX_REG_BINARY_SIZE ) ) {
  
    CWnd::MessageBox( _T("Binary Encryption Test Passed."),
                                _T("Binary Encryption Test"), 
                                MB_OK | MB_ICONASTERISK );
                      
  } else {
  
    CWnd::MessageBox( _T("Binary Encryption Test Failed."),
                                _T("Binary Encryption Test"), 
                                MB_OK | MB_ICONERROR );
  }
}

History

  • 12/03/2007
    • Added Section Advanced Encryption Standard
    • Added Crypto++ Encryption and Decryption
    • Added Reference to Applied Crypto++: Block and Stream Ciphers'
  • 11/16/2006
    • Added Link to Crypto++ Compilation and Integration
    • Removed Section Crypto++ and Visual C++ Compatibility'
  • 10/10/2005 - Initial Release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)